对象
1815字约6分钟
TypeScript
2023-01-30
✨对象类型
对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。
const obj: {
x: number;
y: number;
} = { x: 1, y: 1 };
对象类型限制:
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
不能读写不存在的属性。
不能删除类型声明中存在的属性,修改属性值是可以的。
对象的方法使用函数类型描述:
const obj: {
x: number;
y: number;
add(x: number, y: number): number;
// 或者写成
// add: (x:number, y:number) => number;
} = {
x: 1,
y: 1,
add(x, y) {
return x + y;
},
};
对象类型可以使用方括号读取属性的类型。
type User = {
name: string;
age: number;
};
type Name = User["name"]; // string
注意
注意,TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。
interface MyInterface {
toString(): string; // 继承的属性
prop: number; // 自身的属性
}
// obj只写了prop属性,但是不报错。因为它可以继承原型上面的toString()方法。
const obj: MyInterface = {
// 正确
prop: 123,
};
✨可选属性
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。
const obj: {
x: number;
y?: number;
} = { x: 1 };
读取可选属性之前,必须检查一下是否为undefined
。
// 写法一
let firstName = user.firstName === undefined ? "Foo" : user.firstName;
let lastName = user.lastName === undefined ? "Bar" : user.lastName;
// 写法二
let firstName = user.firstName ?? "Foo";
let lastName = user.lastName ?? "Bar";
✨属性名的索引类型
如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。
索引类型里面,最常见的就是属性名的字符串索引。
type MyObj = {
[property: string]: string;
};
const obj: MyObj = {
foo: "a",
bar: "b",
baz: "c",
};
对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。
type MyType = {
[x: number]: boolean; // 报错
[x: string]: string;
};
// 不报错
type MyType = {
[x: symbol]: string;
[x: number]: string;
};
属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及length
属性,因为类型里面没有定义这些东西。
type MyArr = {
[n: number]: number;
};
const arr: MyArr = [1, 2, 3];
arr.length; // 报错
✨解构赋值
解构赋值的类型写法,跟为对象声明类型是一样的。
const { id, name, price }: { id: string; name: string; price: number; } = product;
注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途:
let { x: foo, y: bar } = obj;
// 等同于
let foo = obj.x;
let bar = obj.y;
✨结构类型原则
只要对象 B
满足 对象 A
的结构特征,TypeScript 就认为对象 B
兼容对象 A
的类型,这称为“结构类型”原则(structual typing)
。
type Person = {
name: string;
};
type Student = {
name: string;
age: number;
}
const student: Student = {
name: '张三',
age: 14
};
const person: Person = student;
如果类型 B
可以赋值给类型 A
,TypeScript 就认为 B
是 A
的子类型(subtyping),A
是 B
的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。
这种设计有时会导致令人惊讶的结果。
type myObj = {
x: number;
y: number;
};
function getSum(obj: myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
const v = obj[n]; // 报错
sum += Math.abs(v);
}
return sum;
}
上面示例中,函数getSum()
要求传入参数的类型是myObj
,但是实际上所有与myObj
兼容的对象都可以传入。这会导致const v = obj[n]
这一行报错,原因是obj[n]
取出的属性值不一定是数值(number)
,使得变量v的类型被推断为any
。如果项目设置为不允许变量类型推断为any,代码就会报错。
解决方法:
- 直接调用变量的属性:
type MyObj = {
x: number;
y: number;
};
function getSum(obj: MyObj) {
return Math.abs(obj.x) + Math.abs(obj.y);
}
- 使用类型断言:
type myObj = {
x: number;
y: number;
};
function getSum(obj: myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
const i = n as keyof myObj;
const v = obj[i]; // 报错
sum += Math.abs(v);
}
return sum;
}
- 索引签名:
type myObj = {
[key: string]: string | number;
x: number;
y: number;
};
function getSum(obj: myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
const v = obj[n];
if (typeof v === 'number') {
sum += Math.abs(v);
}
}
return sum;
}
- 编译选项
关闭noImplicitAny
选项,这样TypeScript就不会默认将表达式的类型视为any
。
✨严格字面量检查
如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。
const point: {
x: number;
y: number;
} = {
x: 1,
y: 1,
z: 1, // 报错
};
如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。
const myPoint = {
x: 1,
y: 1,
z: 1,
};
const point: {
x: number;
y: number;
} = myPoint; // 正确
规避严格字面量检查的方法:
可以使用中间变量。
如果你确认字面量没有错误,也可以使用
as
类型断言规避严格字面量检查。属性的字符串索引([x: string])
编译器选项
suppressExcessPropertyErrors
,可以关闭多余属性检查
✨最小可选属性规则
如果一个对象的所有属性都是可选的,会触发最小可选属性规则。
最小可选属性规则: 当一个对象的所有属性都是可选的,必须至少存在一个可选属性,不能所有可选属性都不存在,且这条规则无法通过中间变量规避。
type Options = {
a?: number;
b?: number;
c?: number;
};
// 报错
const obj: Options = {
d: 123,
};
// 无法通过中间变量规避
const myOptions = { d: 123 };
const obj: Options = myOptions; // 报错
// 设置a后,不报错
const obj: Options = {
a: 23,
d: 123,
};
✨空对象
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
const obj = {}; // 等同于: const obj: {} = {};
obj.prop = 123; // 报错
空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象Object.prototype
的属性。
obj.toString(); // 正确
空对象作为类型,其实是Object
类型的简写形式。
let d: {};
// 等同于
// let d:Object;
d = {};
d = { x: 1 };
d = "hello";
d = 2;
如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)
合成一个新对象。
const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };
const pt = {
...pt0,
...pt1,
...pt2,
};