TypeScript: TypeScript 对象类型

最后更新:2026-08-26

在 JavaScript 中,对象是最常用的数据结构。TypeScript 的对象类型让你精确描述"一个对象应该有什么属性、什么类型"——这是结构化类型系统的核心。

1. 对象类型基础

(1) 匿名对象类型

最直接的写法——直接在类型位置写出对象的结构:

TYPESCRIPT
let user: { name: string; age: number } = {
  name: "Charlie",
  age: 20
};

(2) 类型推断

对象字面量赋值给变量时,TypeScript 会自动推断类型:

TYPESCRIPT
let user = {
  name: "Charlie",
  age: 20
};
// 推断为 { name: string; age: number }

user.name = "Diana";   // ✅
user.age = 21;        // ✅
// user.email = "x@x.com";  // ❌ 推断的类型中没有 email 属性

(3) 多行写法

对象类型属性多时,推荐多行写法(分号或逗号分隔都可以):

TYPESCRIPT
let product: {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
} = {
  id: 1,
  name: "TypeScript权威指南",
  price: 89.9,
  inStock: true
};

▶ 示例:函数参数的对象类型

TYPESCRIPT
function createUser(user: { name: string; age: number; email: string }): string {
  return `${user.name},${user.age}岁,邮箱:${user.email}`;
}

console.log(createUser({
  name: "Charlie",
  age: 20,
  email: "xiaoming@example.com"
}));
▶ 试一试

输出:

TEXT 📖 仅展示
Charlie,20岁,邮箱:xiaoming@example.com

2. 可选属性

? 标记可选属性——该属性可以不存在:

(1) 语法

TYPESCRIPT
let user: { name: string; age?: number } = {
  name: "Charlie"
  // age 可以不提供
};

console.log(user.name);  // "Charlie"
console.log(user.age);   // undefined

(2) 可选属性的类型

可选属性的类型自动包含 undefined

TYPESCRIPT
// age?: number 等价于 age: number | undefined
// 但有重要区别:age?: number 允许属性不存在,age: number | undefined 要求属性必须存在

let config: { host: string; port?: number } = { host: "localhost" };
// config.port 是 undefined(属性不存在也是 ok 的)

let config2: { host: string; port: number | undefined } = { host: "localhost" };
// ❌ 报错!port 属性必须存在,即使是 undefined 也要显式写上
// 正确写法:{ host: "localhost", port: undefined }

(3) 访问可选属性的安全方式

TYPESCRIPT
interface User {
  name: string;
  age?: number;
}

function getAgeText(user: User): string {
  // ❌ 不安全——如果 age 不存在,undefined.toFixed() 会运行时崩溃
  // return user.age.toFixed(0) + "岁";

  // ✅ 方式一:先检查
  if (user.age !== undefined) {
    return user.age.toFixed(0) + "岁";
  }
  return "年龄未知";

  // ✅ 方式二:提供默认值
  // return (user.age ?? 0).toFixed(0) + "岁";
}

3. 只读属性

readonly 标记只读属性——赋值后不可修改:

TYPESCRIPT
let user: { readonly id: number; name: string } = {
  id: 1,
  name: "Charlie"
};

user.name = "Diana";     // ✅ 普通属性可以修改
// user.id = 2;         // ❌ 只读属性不可修改

(2) readonly 是浅层只读

TYPESCRIPT
let config: { readonly data: { value: number } } = {
  data: { value: 42 }
};

// config.data = { value: 100 };  // ❌ data 本身不可重新赋值
config.data.value = 100;          // ✅ data 内部的属性仍然可修改!
💡 深层只读: 如果需要对象所有层级都只读,用 Readonly<类型> 工具类型(嵌套使用)或 as const 断言。第19课会介绍 ReadonlyPartialRequired 等工具类型。


4. 索引签名

当对象的属性名不确定,但属性值的类型确定时,用索引签名:

(1) 字符串索引签名

TYPESCRIPT
let scores: { [key: string]: number } = {
  语文: 90,
  数学: 95,
  英语: 88
};

console.log(scores["数学"]);   // 95

// 添加新属性——只要是 string 键 + number 值就行
scores["物理"] = 85;   // ✅

(2) 数字索引签名

TYPESCRIPT
let arr: { [index: number]: string } = {
  0: "first",
  1: "second",
  2: "third"
};

console.log(arr[1]);  // "second"

(3) 索引签名的限制

索引签名要求所有已知属性的类型必须是索引值类型的子类型:

TYPESCRIPT
// ❌ 错误:name 是 string,但索引签名要求所有值是 number
// let config: { [key: string]: number; name: string } = { name: "test" };

// ✅ 正确:所有值类型一致
let config: { [key: string]: string; name: string } = { name: "test" };

(4) 模板字面量索引签名(TypeScript 4.4+)

TYPESCRIPT
let css: { [key: `--${string}`]: string } = {
  "--primary-color": "#4A90D9",
  "--font-size": "14px"
};

▶ 示例:用索引签名实现动态配置

TYPESCRIPT
interface EnvConfig {
  [key: string]: string | number | boolean;
  // 所有值可以是 string、number 或 boolean
  // 已知属性也必须兼容
  NODE_ENV: string;
  PORT: number;
}

let env: EnvConfig = {
  NODE_ENV: "development",
  PORT: 3000,
  DEBUG: true,        // ✅ 动态属性
  API_URL: "http://localhost:8080"  // ✅ 动态属性
};

console.log(`环境:${env.NODE_ENV}`);
console.log(`端口:${env.PORT}`);
console.log(`调试:${env.DEBUG}`);
▶ 试一试

输出:

TEXT 📖 仅展示
环境:development
端口:3000
调试:true

5. 嵌套对象类型

实际开发中,对象经常多层嵌套——TypeScript 支持逐层描述类型:

(1) 嵌套对象字面量类型

TYPESCRIPT
let user: {
  name: string;
  address: {
    city: string;
    zip: string;
  };
  tags: string[];
} = {
  name: "Charlie",
  address: {
    city: "北京",
    zip: "100000"
  },
  tags: ["开发者", "TypeScript"]
};

console.log(user.address.city);  // "北京"
console.log(user.tags[0]);       // "开发者"

(2) 多层嵌套

TYPESCRIPT
type ApiResponse = {
  status: number;
  data: {
    user: {
      id: number;
      profile: {
        avatar: string;
        bio: string;
      };
    };
  };
};

let response: ApiResponse = {
  status: 200,
  data: {
    user: {
      id: 1,
      profile: {
        avatar: "https://example.com/avatar.png",
        bio: "TypeScript 爱好者"
      }
    }
  }
};

console.log(response.data.user.profile.bio);  // "TypeScript 爱好者"
💡 建议: 嵌套超过2-3层时,应该把内层类型提取为独立的 interface 或 type 别名,增强可读性和复用性。第9-11课会详细讲解。


6. 对象类型的两种风格

(1) 匿名类型(内联)

TYPESCRIPT
function greet(user: { name: string; age: number }): string {
  return `你好,${user.name}!`;
}

适合:只用一次的简单类型、函数参数

(2) 命名类型(interface / type)

TYPESCRIPT
interface User {
  name: string;
  age: number;
}

function greet(user: User): string {
  return `你好,${user.name}!`;
}

适合:多处复用、属性较多、需要继承/扩展

📌 建议: 属性超过3个或类型需要复用时,提取为 interface 或 type 别名。简单的一次性参数可以用匿名类型。


7. 结构化类型系统

TypeScript 采用结构化类型系统——只要对象的结构满足类型要求就兼容,不要求类型名称相同:

(1) 结构匹配

TYPESCRIPT
interface Point {
  x: number;
  y: number;
}

let point: Point = { x: 1, y: 2 };   // ✅ 有 x 和 y

let point3D = { x: 1, y: 2, z: 3 };
point = point3D;   // ✅ point3D 有 x 和 y,多余属性不冲突

(2) 多余属性检查

对象字面量直接赋值时,TypeScript 会执行"多余属性检查"——不允许有目标类型中不存在的属性:

TYPESCRIPT
interface Point {
  x: number;
  y: number;
}

// ❌ 多余属性检查——直接赋值对象字面量时不允许有额外属性
// let p: Point = { x: 1, y: 2, z: 3 };

// ✅ 方式一:通过变量绕过(因为变量不会触发多余属性检查)
let temp = { x: 1, y: 2, z: 3 };
let p: Point = temp;   // ✅

// ✅ 方式二:用类型断言
let p2: Point = { x: 1, y: 2, z: 3 } as Point;

// ✅ 方式三:扩展接口(最推荐)
interface Point3D extends Point {
  z: number;
}
let p3: Point3D = { x: 1, y: 2, z: 3 };
💡 为什么要多余属性检查? 这是为了捕获拼写错误。如果你写 { x: 1, y: 2, zzz: 3 },TypeScript 会立刻提醒你——zzz 不是 Point 的属性,你是不是拼错了?

▶ 示例:结构化类型实战

TYPESCRIPT
interface Printable {
  toString(): string;
}

// Date 有 toString 方法,结构匹配
let date: Printable = new Date();

// 自定义对象有 toString 方法,也匹配
let custom: Printable = {
  toString() { return "自定义对象"; }
};

function print(obj: Printable): void {
  console.log(obj.toString());
}

print(date);    // 输出 Date 的字符串表示
print(custom);  // 输出 "自定义对象"
▶ 试一试

❓ 常见问题

Q 对象类型用 interface 还是 type 别名?
A 两者大部分场景等价。interface 更适合定义对象形状(支持声明合并、extends 继承),type 更适合联合类型、条件类型等复杂操作。团队统一即可,本章后续会详细对比。初学者推荐从 interface 开始。
Q 可选属性和 undefined 联合类型有什么区别?
A age?: number 允许属性不存在(完全缺失),age: number | undefined 要求属性必须存在但值可以是 undefined。实际开发中,可选属性更常用——省略属性比显式写 undefined 更自然。
Q 为什么对象字面量有多余属性检查,变量却没有?
A 对象字面量直接赋值时,开发者通常很清楚自己写了什么——多余属性大概率是拼写错误。变量赋值时,变量的类型可能来自其他地方,多余属性可能是合理的(子类型多出的属性)。这是 TypeScript 在"安全"和"灵活"之间的权衡。
Q readonly 属性真的不可修改吗?
A readonly 只是编译时检查,运行时没有任何防护。通过类型断言或 any 可以绕过。但它能有效防止"意外修改"——这是类型系统的主要目的:在编译时捕获错误,而不是运行时防护。

📖 小节

📝 作业

  1. 基础题(难度⭐):定义一个对象类型描述"图书"(title、author、price、inStock?),创建两本书的对象——一本有 inStock,一本没有,输出它们的标题和价格。
  2. 进阶题(难度⭐⭐):写一个函数 updateUser(user: { name: string; age: number; email?: string }, updates: { age?: number; email?: string }),返回更新后的用户对象。要求原 user 的 name 不可变(用 readonly)。
  3. 挑战题(难度⭐⭐⭐):用索引签名定义一个 CacheStore 类型,键是 string,值是 { data: T; timestamp: number }(T 是泛型)。创建一个 CacheStore<string> 实例,存入两条缓存数据,然后遍历输出所有缓存的键和过期状态(假设5秒过期)。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏