TypeScript 的对象类型
简介
除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。
对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。
type MyObj = {
x: number,
y: number,
add: (x:number, y:number) => number;
};一旦声明了对象的类型:
- 对象赋值时,就不能缺少属性,也不能有多余属性。
- 读写不存在的属性也会报错,也不能删除类型声明中存在的属性。
- 可以修改属性的值。
上面示例中,对象obj有一个方法add(),需要定义它的参数类型和返回值类型。
除了type命令可以为对象类型声明一个别名,TypeScript 还提供了interface命令,可以把对象类型提炼为一个接口。
注意,TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。
interface MyInterface {
toString(): string; // 继承的属性
prop: number; // 自身的属性
}
const obj: MyInterface = {
// 正确
prop: 123,
};上面示例中,obj只写了prop属性,但是不报错。因为它可以继承原型上面的toString()方法。
可选属性
type User = {
firstName: string;
lastName?: string;
};
// 等同于
type User = {
firstName: string;
lastName: string | undefined;
};所以,读取可选属性之前,必须检查一下是否为undefined。
const user: {
firstName: string;
lastName?: string;
} = { firstName: "Foo" };
// 原始写法
if (user.lastName !== undefined) {
console.log(`hello ${user.firstName} ${user.lastName}`);
}
// 写法一:三元运算符
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";只读属性
interface Point = {
readonly x: number;
readonly y: number;
};
const p: Point = { x: 0, y: 0 };
p.x = 100; // 报错上面示例中,类型Point的属性x和y都带有修饰符readonly,表示这两个属性只能在初始化期间赋值,后面再修改就会报错。
如果只读属性的值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。
实现只读属性的另一种方法是在对象后面加上只读断言as const。
const myUser = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 报错属性名的索引类型
如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。
type MyObj = {
[property: string]: string;
};
const obj: MyObj = {
foo: "a",
bar: "b",
baz: "c",
};JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有number和symbol。
type T1 = {
[property: number]: string;
};
type T2 = {
[property: symbol]: string;
};
type MyArr = {
[n: number]: number;
};
const arr: MyArr = [1, 2, 3];
// 或者
const arr: MyArr = {
0: 1,
1: 2,
2: 3,
};对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。
type MyType = {
[x: number]: boolean; // 报错
[x: string]: string;
};同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错。
type MyType = {
foo: boolean; // 报错
[x: string]: string;
};属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。
解构赋值
解构赋值用于直接从对象中提取属性。
const { id, name, price } = product;上面语句从对象product提取了三个属性,并声明属性名的同名变量。
解构赋值的类型写法,跟为对象声明类型是一样的。
const {
id,
name,
price,
}: {
id: string;
name: string;
price: number;
} = product;上面示例中,冒号不是表示属性x和y的类型,而是为这两个属性指定新的变量名。如果要为x和y指定类型,不得不写成下面这样。
let { x: foo, y: bar }: { x: string; y: number } = obj;下面的写法是错误的,和 js 里边结构并赋新变量名冲突了。
function draw({ shape: Shape, xPos: number = 100 }) {
let myShape = shape; // 报错
let x = xPos; // 报错
}上面示例中,函数draw()的参数是一个对象解构,里面的冒号很像是为变量指定类型,其实是为对应的属性指定新的变量名。所以,TypeScript 就会解读成,函数体内不存在变量shape,而是属性shape的值被赋值给了变量Shape。
严格字面量检查
如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。
let o = {x: 1, y: 2, z: 3}
const point: {
x: number;
y: number;
} = o // 正确的
const point2: {
x: number;
y: number;
} = { x: 1, y: 2, z: 3} // 错误的,严格字面量检查未通过上面示例中,等号右边是一个对象的字面量,这时会触发严格字面量检查。只要有类型声明中不存在的属性(本例是z),就会导致报错。
TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。
所以,开发时,可以使用中间变量规避严格字面量检查,也可以使用类型断言规避严格字面量检查。
const point2: PointXY = { x: 1, y: 2, z: 3} as PointXY编译器选项suppressExcessPropertyErrors,可以关闭多余属性检查。下面是它在 tsconfig.json 文件里面的写法。
{
"compilerOptions": {
"suppressExcessPropertyErrors": true
}
}最小可选属性规则
如果一个对象的所有属性都是可选的,会触发最小可选属性规则,即必须至少存在一个可选属性,不能所有可选属性都不存在
type Options = {
a?: number;
b?: number;
c?: number;
};
const obj: Options = {
d: 123, // 报错
};
const o: Options = {
c: 123, // OK
};空对象
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
变量obj的值是一个空对象,然后对obj.prop赋值就会报错。空对象只能使用继承的属性,即继承自原型对象Object.prototype的属性。
const obj = {};
obj.prop = 123; // 报错
obj.toString(); // 正确回到本节开始的例子,这种写法其实在 JavaScript 很常见:先声明一个空对象,然后向空对象添加属性。但是,TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。
在 TS 中,如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)合成一个新对象。
const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };
const pt = {
...pt0,
...pt1,
...pt2,
};上面示例中,对象pt是三个部分合成的,这样既可以分步声明,也符合 TypeScript 静态声明的要求。
空对象作为类型,其实是Object类型的简写形式。
let d: {}; // 等价于 let d:Object;
d = {};
d = { x: 1 };
d = "hello";
d = 2;上面示例中,各种类型的值(除了null和undefined)都可以赋值给空对象类型,跟Object类型的行为是一样的。
如果想强制使用没有任何属性的对象,可以采用下面的写法。
interface WithoutProperties {
[key: string]: never; // 值为 never
}
// 报错
const a: WithoutProperties = { prop: 1 };