TypeScript 全面详解:对象类型的语法规则与实战指南
🔥全面解析 TypeScript 对象类型的语法细节和使用规范。
一、对象类型的基础声明
1. 直接字面量声明
对象类型最简单的声明方式,就是使用大括号 {} 包裹,内部逐一声明每个属性的名称和对应类型,这是最直观的对象类型定义方式。
typescript
// 变量后直接声明对象类型
const obj: {
x: number;
y: number;
} = { x: 1, y: 1 };
上述示例中,变量 obj 的类型直接写在变量名后,大括号内明确约束了 x 和 y 两个属性均为 number 类型,赋值时必须严格匹配该类型结构。
2. 属性分隔符规则
对象类型内部的属性之间,支持分号 ; 或逗号 , 作为分隔符,两种写法效果完全一致,可根据个人或团队编码风格选择。
typescript
// 写法 1:分号分隔(推荐,更清晰区分属性与类型结束)
type MyObj1 = {
x: number;
y: number;
};
// 写法 2:逗号分隔(贴近 JavaScript 对象字面量写法)
type MyObj2 = {
x: number,
y: number,
};
另外,最后一个属性后面的分隔符是可选的,可写可不写,不会影响类型校验。
typescript
// 合法:最后一个属性后无分隔符
type MyObj3 = {
x: number;
y: number
};
3. 严格的属性匹配规则
一旦声明了对象类型,赋值时就必须严格遵守「不多不少」的原则:不能缺少声明过的属性,也不能额外添加未声明的属性。
typescript
type MyObj = {
x: number;
y: number;
};
// 错误:缺少属性 y(Property 'y' is missing in type '{ x: number; }')
const o1: MyObj = { x: 1 };
// 错误:多余属性 z(Object literal may only specify known properties)
const o2: MyObj = { x: 1, y: 1, z: 1 };
这种严格校验仅针对「对象字面量直接赋值」,如果是将对象赋值给一个变量后再传递,多余属性不会报错(这是 TS 的「对象字面量新鲜度」规则),但仍不推荐这样做。
4. 属性的读写与删除规则
- 读写未声明的属性:会直接报错,TS 不允许访问对象类型中不存在的属性。
- 删除已声明的属性:会报错,TS 不允许删除类型声明中明确存在的属性。
- 修改已声明属性的值:合法,只要值的类型与声明类型一致即可。
typescript
const obj: {
x: number;
y: number;
} = { x: 1, y: 1 };
// 错误:读取不存在的属性 z(Property 'z' does not exist on type '{ x: number; y: number; }')
console.log(obj.z);
// 错误:写入不存在的属性 z(Property 'z' does not exist on type '{ x: number; y: number; }')
obj.z = 1;
const myUser: { name: string } = {
name: "Sabrina",
};
// 错误:删除已声明的属性 name(The operand of a 'delete' operator must be optional)
delete myUser.name;
// 正确:修改属性值(类型匹配)
myUser.name = "Cynthia";
二、对象方法的类型声明
对象中的方法可以通过两种方式声明类型:函数签名语法 和 箭头函数语法,两种写法效果一致,推荐使用函数签名语法(更贴近 JavaScript 方法的书写习惯)。
typescript
const obj: {
x: number;
y: number;
// 写法 1:函数签名语法(推荐)
add(x: number, y: number): number;
// 写法 2:箭头函数语法
subtract: (x: number, y: number) => number;
} = {
x: 1,
y: 1,
add(x, y) {
return x + y;
},
subtract(x, y) {
return x - y;
}
};
上述示例中,方法 add 和 subtract 均声明了参数类型(两个 number 类型)和返回值类型(number 类型),实现时必须严格遵循该类型约束。
三、对象属性类型的读取
TypeScript 支持使用方括号 [] 读取对象类型中某个属性的具体类型,这在提取单个属性类型、实现类型复用场景中非常有用。
typescript
// 定义完整对象类型
type User = {
name: string;
age: number;
gender: boolean;
};
// 读取 name 属性的类型 → string
type UserName = User['name'];
// 读取 age 属性的类型 → number
type UserAge = User['age'];
// 实际使用:约束变量类型为 User 的 name 属性类型
const userName: UserName = "张三";
const userAge: UserAge = 22;
注意,方括号内的属性名必须用引号包裹(字符串字面量),且必须是对象类型中已声明的属性,否则会报错。
四、对象类型的两种别名方式:type vs interface
除了直接字面量声明,TypeScript 还提供了两种方式将对象类型提炼为可复用的别名:type 命令和 interface 命令,两者在对象类型声明场景中功能相似。
1. 两种方式的基本使用
typescript
// 写法 1:type 命令(类型别名)
type MyObjType = {
x: number;
y: number;
};
const obj1: MyObjType = { x: 1, y: 1 };
// 写法 2:interface 命令(接口)
interface MyObjInterface {
x: number;
y: number;
}
const obj2: MyObjInterface = { x: 1, y: 1 };
2. 接口的特殊特性:继承属性的兼容
TypeScript 不区分对象「自身属性」和「继承属性」,一律视为对象的合法属性。因此,在接口中声明的继承属性(如 toString()),实现时无需手动提供(可从原型链继承)。
typescript
interface MyInterface {
// 继承属性:从 Object 原型链继承
toString(): string;
// 自身属性:必须手动提供
prop: number;
}
// 正确:仅提供自身属性 prop,toString() 从原型链继承
const obj: MyInterface = {
prop: 123,
};
上述示例中,obj 仅提供了 prop 属性,但仍符合 MyInterface 接口约束,因为 toString() 方法可从 Object 原型链中继承,无需手动实现。
五、可选属性
1. 可选属性的声明:?
如果某个属性不是必需的(可存在、可不存在),可以在属性名后添加问号 ? 标记为可选属性。
typescript
// 声明 y 为可选属性
const obj: {
x: number;
y?: number;
} = { x: 1 }; // 正确:无需提供 y 属性
2. 可选属性与 undefined 的等价性
可选属性本质上等同于「允许赋值为 undefined 的属性」,下面两种写法完全等效:
typescript
// 写法 1:简洁写法
type User1 = {
firstName: string;
lastName?: string;
};
// 写法 2:完整写法(明确允许 undefined)
type User2 = {
firstName: string;
lastName: string | undefined;
};
因此,可选属性可以显式赋值为 undefined,不会报错:
typescript
const obj: {
x: number;
y?: number;
} = { x: 1, y: undefined }; // 正确
3. 可选属性的使用注意事项
读取未赋值的可选属性时,返回值为 undefined,直接调用该属性的方法会报错,因此使用前必须先进行 undefined 校验。
typescript
type MyObj = {
x: string;
y?: string;
};
const obj: MyObj = { x: 'hello' };
// 错误:Cannot read properties of undefined (reading 'toLowerCase')
obj.y.toLowerCase();
常用的校验方式有两种:条件判断或 Null 合并运算符 ??(推荐)。
typescript
const user: {
firstName: string;
lastName?: string;
} = { firstName: 'Foo' };
// 方式 1:条件判断
if (user.lastName !== undefined) {
console.log(`hello ${user.firstName} ${user.lastName}`);
}
// 方式 2:Null 合并运算符(设置默认值,更简洁)
const firstName = user.firstName ?? '默认姓名';
const lastName = user.lastName ?? '默认姓氏';
console.log(`hello ${firstName} ${lastName}`);
4. 严格可选属性配置:ExactOptionalPropertyTypes
TypeScript 提供了编译配置 ExactOptionalPropertyTypes,当该配置与 strictNullChecks 同时开启时,可选属性将不允许显式赋值为 undefined,仅允许省略不写。
typescript
// 开启 ExactOptionalPropertyTypes 和 strictNullChecks 后
const obj: {
x: number;
y?: number;
} = { x: 1, y: undefined }; // 错误:Type 'undefined' is not assignable to type 'number | undefined'
5. 可选属性 vs 允许 undefined 的必选属性
两者看似相似,但存在核心区别:可选属性可省略不写,而允许 undefined 的必选属性必须显式提供(即使值为 undefined)。
typescript
type A = { x: number; y?: number }; // y 是可选属性
type B = { x: number; y: number | undefined }; // y 是必选属性(允许 undefined)
const objA: A = { x: 1 }; // 正确:可选属性可省略
const objB: B = { x: 1 }; // 错误:缺少必选属性 y(Property 'y' is missing in type '{ x: number; }')
const objB2: B = { x: 1, y: undefined }; // 正确:显式提供 y 并赋值为 undefined
六、只读属性
1. 只读属性的声明:readonly
在属性名前添加**readonly 关键字**,可将该属性标记为只读属性,初始化赋值后无法再修改其值。
typescript
// 接口中声明只读属性
interface MyInterface {
readonly prop: number;
}
// 直接字面量声明只读属性
const person: {
readonly age: number;
} = { age: 20 };
// 错误:Cannot assign to 'age' because it is a read-only property
person.age = 21;
只读属性仅能在对象初始化期间赋值,此后任何修改操作都会被 TS 禁止。
typescript
type Point = {
readonly x: number;
readonly y: number;
};
// 初始化赋值(合法)
const p: Point = { x: 0, y: 0 };
// 后续修改(非法)
p.x = 100; // 错误
2. 只读属性的深层注意事项
readonly 修饰符仅约束「属性本身的赋值」,如果属性值是一个对象(引用类型),readonly 不会禁止修改该对象的内部属性,仅禁止完全替换该对象。
typescript
interface Home {
readonly resident: {
name: string;
age: number;
};
}
const h: Home = {
resident: {
name: 'Vicky',
age: 42
}
};
// 正确:修改对象内部属性(readonly 不约束深层属性)
h.resident.age = 32;
// 错误:完全替换 readonly 属性的值(Cannot assign to 'resident' because it is a read-only property)
h.resident = {
name: 'Kate',
age: 23
};
3. 只读引用的相互影响
如果一个对象有两个引用(一个可写、一个只读),修改可写引用的属性,会影响到只读引用(因为两者指向同一个对象)。
typescript
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
// 可写引用
let writablePerson: Person = {
name: 'Vicky',
age: 42,
};
// 只读引用(指向同一个对象)
let readonlyPerson: ReadonlyPerson = writablePerson;
// 修改可写引用的属性
writablePerson.age += 1;
// 只读引用的属性值也会变化(输出 43)
console.log(readonlyPerson.age);
4. 只读断言:as const
除了声明时使用 readonly,还可以在对象赋值时使用**as const 只读断言**,将整个对象转为只读对象,所有属性均无法修改。
typescript
const myUser = {
name: "Sabrina",
} as const;
// 错误:Cannot assign to 'name' because it is a read-only property
myUser.name = "Cynthia";
注意:如果变量已经明确声明了类型,TS 会以「声明的类型」为准,as const 断言会被忽略。
typescript
// 明确声明类型:name 为可写的 string 类型
const myUser: { name: string } = {
name: "Sabrina",
} as const;
// 正确:以声明的类型为准,name 可修改
myUser.name = "Cynthia";
七、属性名的索引类型
当对象的属性名无法提前确定(如 API 返回的动态数据),或属性数量过多时,TypeScript 允许使用「属性名的索引类型」(又称索引签名)来描述对象类型,支持字符串、数字、Symbol 三种索引类型。
1. 字符串索引类型
最常用的索引类型,用于约束「属性名为字符串、属性值为统一类型」的对象。
typescript
// 字符串索引类型:属性名是 string,属性值是 string
type StringMap = {
[property: string]: string;
};
// 合法:所有属性均符合索引类型约束
const language: StringMap = {
zh: "中文",
en: "英文",
ja: "日文",
};
其中,[property: string] 中的 property 是自定义的变量名,可任意修改(如 [key: string]),不影响类型校验。
2. 数字索引类型
用于约束「属性名为数字、属性值为统一类型」的对象,常用来描述类数组对象。
typescript
// 数字索引类型:属性名是 number,属性值是 number
type NumberMap = {
[index: number]: number;
};
// 合法:类数组对象
const arrLike: NumberMap = {
0: 1,
1: 2,
2: 3,
};
// 也可用于简单数组(不推荐,数组有专门的类型声明方式)
const simpleArr: NumberMap = [1, 2, 3];
3. 索引类型的约束规则
- 数字索引服从字符串索引:JavaScript 内部会将数字属性名自动转为字符串,因此当对象同时存在数字索引和字符串索引时,数字索引的属性值类型必须兼容字符串索引的属性值类型,否则会报错。
typescript
// 错误:数字索引类型(boolean)与字符串索引类型(string)不兼容
type MyType = {
[x: number]: boolean;
[x: string]: string;
};
// 正确:数字索引类型(string)兼容字符串索引类型(string)
type MyType2 = {
[x: number]: string;
[x: string]: string;
};
- 具体属性必须兼容索引类型:如果对象同时声明了具体属性和索引类型,具体属性的类型必须兼容索引类型的属性值类型,否则会报错。
typescript
// 错误:具体属性 foo(boolean)与索引类型(string)不兼容
type MyType3 = {
foo: boolean;
[x: string]: string;
};
// 正确:具体属性 foo(string)兼容索引类型(string)
type MyType4 = {
foo: string;
[x: string]: string;
};
4. 索引类型的使用注意事项
索引类型的约束较为宽泛,容易忽略具体属性的细节校验,建议谨慎使用。另外,数字索引类型不宜用来声明数组,因为这种方式无法支持数组的 length 属性和各类数组方法(如 push()、map())。
typescript
type MyArr = {
[n: number]: number;
};
const arr: MyArr = [1, 2, 3];
// 错误:Property 'length' does not exist on type 'MyArr'
console.log(arr.length);
// 错误:Property 'push' does not exist on type 'MyArr'
arr.push(4);
八、对象的解构赋值与类型声明
TypeScript 支持 JavaScript 的对象解构赋值,同时也需要为解构后的变量添加类型约束,其类型写法与对象类型声明一致。
1. 基本解构赋值的类型声明
typescript
// 定义对象类型
type Product = {
id: string;
name: string;
price: number;
};
// 定义原始对象
const product: Product = {
id: "P001",
name: "TypeScript 实战指南",
price: 59.9,
};
// 解构赋值并添加类型声明
const { id, name, price }: {
id: string;
name: string;
price: number;
} = product;
// 或直接使用已定义的类型别名(推荐,更简洁)
const { id: productId, name: productName }: Product = product;
2. 解构赋值中的冒号陷阱
需要特别注意:对象解构中的冒号 : 不是用来指定类型的,而是用来为属性重命名的。如果要为解构变量指定类型,必须在解构表达式外部整体添加类型声明。
typescript
const obj = { x: "hello", y: 123 };
// 解构重命名:x → foo,y → bar(冒号不是类型声明)
let { x: foo, y: bar } = obj;
// 等同于
let foo = obj.x;
let bar = obj.y;
// 正确:外部添加整体类型声明
let { x: foo2, y: bar2 }: { x: string; y: number } = obj;
3. 函数参数解构的类型声明
在函数参数中使用对象解构时,同样需要注意冒号的用途,避免将重命名误认为类型声明。
typescript
// 错误示例:将重命名误认为类型声明
function draw({
shape: Shape, // 重命名:shape → Shape
xPos: number = 100 // 错误:无法将类型 'number' 赋值给变量 xPos
}) {
let myShape = shape; // 错误:找不到变量 shape
let x = xPos; // 错误:找不到变量 xPos
}
// 正确示例:外部添加类型声明
function drawCorrect({
shape,
xPos = 100
}: {
shape: string;
xPos: number;
}) {
let myShape = shape;
let x = xPos;
console.log(`绘制${myShape},x坐标:${x}`);
}
九、核心总结
- TypeScript 对象类型的基础声明使用
{},属性分隔符支持;或,,赋值时需严格匹配「不多不少」的属性规则。 - 对象方法支持函数签名和箭头函数两种类型声明方式,属性类型可通过
[]读取。 type和interface均可定义对象类型别名,interface支持继承属性的兼容。- 可选属性用
?声明,本质等同于允许undefined,使用前需做undefined校验;只读属性用readonly声明,仅约束属性本身的赋值。 - 索引类型用于处理动态属性名,支持字符串、数字、Symbol 三种类型,需遵循「数字索引服从字符串索引」的规则。
- 对象解构赋值的类型声明需在外部整体添加,解构内部的冒号用于重命名,而非类型指定。