Skip to content

TypeScript 的对象类型

简介

除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。

对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。

ts
type MyObj = {
  x: number,
  y: number,
  add: (x:number, y:number) => number;
};

一旦声明了对象的类型:

  • 对象赋值时,就不能缺少属性,也不能有多余属性。
  • 读写不存在的属性也会报错,也不能删除类型声明中存在的属性。
  • 可以修改属性的值。

上面示例中,对象obj有一个方法add(),需要定义它的参数类型和返回值类型。

除了type命令可以为对象类型声明一个别名,TypeScript 还提供了interface命令,可以把对象类型提炼为一个接口。

注意,TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。

ts
interface MyInterface {
  toString(): string; // 继承的属性
  prop: number; // 自身的属性
}

const obj: MyInterface = {
  // 正确
  prop: 123,
};

上面示例中,obj只写了prop属性,但是不报错。因为它可以继承原型上面的toString()方法。

可选属性

ts
type User = {
  firstName: string;
  lastName?: string;
};

// 等同于
type User = {
  firstName: string;
  lastName: string | undefined;
};

所以,读取可选属性之前,必须检查一下是否为undefined

ts
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";

只读属性

ts
interface Point = {
  readonly x: number;
  readonly y: number;
};

const p: Point = { x: 0, y: 0 };

p.x = 100; // 报错

上面示例中,类型Point的属性xy都带有修饰符readonly,表示这两个属性只能在初始化期间赋值,后面再修改就会报错。

如果只读属性的值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象

实现只读属性的另一种方法是在对象后面加上只读断言as const

ts
const myUser = {
  name: "Sabrina",
} as const;

myUser.name = "Cynthia"; // 报错

属性名的索引类型

如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。

ts
type MyObj = {
  [property: string]: string;
};

const obj: MyObj = {
  foo: "a",
  bar: "b",
  baz: "c",
};

JavaScript 对象的属性名(即上例的property)的类型有三种可能,除了上例的string,还有numbersymbol

ts
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 语言内部,所有的数值属性名都会自动转为字符串属性名

ts
type MyType = {
  [x: number]: boolean; // 报错
  [x: string]: string;
};

同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错

ts
type MyType = {
  foo: boolean; // 报错
  [x: string]: string;
};

属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。

解构赋值

解构赋值用于直接从对象中提取属性。

ts
const { id, name, price } = product;

上面语句从对象product提取了三个属性,并声明属性名的同名变量。

解构赋值的类型写法,跟为对象声明类型是一样的。

ts
const {
  id,
  name,
  price,
}: {
  id: string;
  name: string;
  price: number;
} = product;

上面示例中,冒号不是表示属性xy的类型,而是为这两个属性指定新的变量名。如果要为xy指定类型,不得不写成下面这样。

ts
let { x: foo, y: bar }: { x: string; y: number } = obj;

下面的写法是错误的,和 js 里边结构并赋新变量名冲突了。

ts
function draw({ shape: Shape, xPos: number = 100 }) {
  let myShape = shape; // 报错
  let x = xPos; // 报错
}

上面示例中,函数draw()的参数是一个对象解构,里面的冒号很像是为变量指定类型,其实是为对应的属性指定新的变量名。所以,TypeScript 就会解读成,函数体内不存在变量shape,而是属性shape的值被赋值给了变量Shape

严格字面量检查

如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。

ts
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。

所以,开发时,可以使用中间变量规避严格字面量检查,也可以使用类型断言规避严格字面量检查。

ts
const point2: PointXY = { x: 1, y: 2, z: 3} as PointXY

编译器选项suppressExcessPropertyErrors,可以关闭多余属性检查。下面是它在 tsconfig.json 文件里面的写法。

json
{
  "compilerOptions": {
    "suppressExcessPropertyErrors": true
  }
}

最小可选属性规则

如果一个对象的所有属性都是可选的,会触发最小可选属性规则,即必须至少存在一个可选属性,不能所有可选属性都不存在

ts
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的属性。

ts
const obj = {};
obj.prop = 123; // 报错

obj.toString(); // 正确

回到本节开始的例子,这种写法其实在 JavaScript 很常见:先声明一个空对象,然后向空对象添加属性。但是,TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。

在 TS 中,如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)合成一个新对象。

ts
const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };

const pt = {
  ...pt0,
  ...pt1,
  ...pt2,
};

上面示例中,对象pt是三个部分合成的,这样既可以分步声明,也符合 TypeScript 静态声明的要求。

空对象作为类型,其实是Object类型的简写形式。

ts
let d: {}; // 等价于 let d:Object;

d = {};
d = { x: 1 };
d = "hello";
d = 2;

上面示例中,各种类型的值(除了nullundefined)都可以赋值给空对象类型,跟Object类型的行为是一样的。

如果想强制使用没有任何属性的对象,可以采用下面的写法。

ts
interface WithoutProperties {
  [key: string]: never; // 值为 never
}

// 报错
const a: WithoutProperties = { prop: 1 };