封面为 Bing AI 画图:熊猫在端午节赛龙舟
在 JavaScript 中,我们分类数据、传递数据的基本方式是通过对象。在 TypeScript 中,通过对象类型来表示对象的类型。
对象类型可以是匿名的:
function greet(person: { name: string; age: number }) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return "Hello " + person.name;
}
也可以通过接口来命名:
interface Person {
// ^^^^^^
name: string;
age: number;
}
function greet(person: Person) {
return "Hello " + person.name;
}
或类型别名:
type Person = {
// ^^^^^^
name: string;
age: number;
};
function greet(person: Person) {
return "Hello " + person.name;
}
以上三个示例中,我们编写了接受包含属性 name (必须是 string )和 age (必须是 number )的对象的函数。
快速参考
如果您想快速浏览一下重要的日常语法,可以浏览 type 和 interface 的备忘录。
属性修改器
对象类型中的每个属性都可以指定:属性类型、属性是否可选以及属性是否可以写入。
可选属性
在属性后面加上?代表此属性是可选的。
interface Shape {}
declare function getShape(): Shape;
// ---cut---
interface PaintOptions {
shape: Shape;
xPos?: number;
// ^
yPos?: number;
// ^
}
function paintShape(opts: PaintOptions) {
// ...
}
const shape = getShape();
paintShape({ shape });
paintShape({ shape, xPos: 100 });
paintShape({ shape, yPos: 100 });
paintShape({ shape, xPos: 100, yPos: 100 });
在此示例中, xPos 和 yPos 都被视为可选。可以提供它们中的任何一个,所以上面 paintShape 的每个调用都是有效的。
但是当我们在存在 strictNullChecks 配置下读取时,TypeScript 会告诉我们它们可能是 undefined 。
在 JavaScript 中,即使没有设置该属性,我们仍然可以访问它,但它是 undefined 。
这种为未指定值设置默认值的模式非常普遍,JavaScript 有语法来支持它。
这里,我们为 paintShape 的参数使用了解构模式,并为 xPos 和 yPos 提供了默认值。现在 xPos 和 yPos 都肯定存在于 paintShape 的主体中,但对于调用 paintShape 函数的人,xPos 和 yPos都是可选的
请注意,目前无法在解构模式中放置类型注释。因为以下语法在 JavaScript 中意味着其他内容。
在这里,Shape 表示获取属性 shape 并将其在本地重新定义为名为 Shape 的变量。同样, xPos: number 创建了一个名为 number 的变量,其值基于参数的 xPos 。
只读属性
TypeScript 中,属性也可以标记为 readonly 。虽然它不会在运行时更改任何行为,但在类型检查期间不能写入标记为 readonly 的属性。
使用 readonly 修饰符不一定意味着一个值深层完全不可变:
如上 resident 是只读的,直接更改 resident 会报错提示,但是更改 resident.age 就不会报错。
管理 readonly 的预期很重要。属性是 readonly 并不代表它不能被改变,类型之间的判断主要是形状上的, TypeScript 不会检查 readonly 修饰。
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};
// works
let readonlyPerson: ReadonlyPerson = writablePerson;
console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'
虽然 readonlyPerson 属性有 readonly 修饰,但是他的属性还是可以被改变。
索引签名
有时你并不能提前知道类型属性的所有名称,但你知道值的模糊形状。
在此情况下,你可以使用索引签名来描述可能值的类型,例如:
上面,我们有一个 StringArray 接口,它有一个索引签名。 这个索引签名表明当一个 StringArray 被一个 number 索引时,它将返回一个 string。
索引签名属性只允许使用这些类型: string、number、symbol、模板字符串,或仅由这些组成的联合类型。(因为索引就只能是这几种类型,没有其他类型了)
可以支持两种类型的索引器,但从数字索引器返回的类型必须是从字符串索引器返回的类型的子类型。 这是因为当使用 number 进行索引时,JavaScript 实际上会在索引到对象之前将其转换为 string。 这意味着使用 100(一个 number)进行索引与使用 "100"(一个 string)进行索引是一样的.
[x: number] 的类型范围比 [x: string] 大,所以报错了,如果 [x: number] 是 Dog 并且 [x: string] 是 Animal,那么就不报错了。
你写的索引签名要保证符合所有属性类型,否则会报错:
当然,索引签名也可以是属性类型的联合:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
索引签名也可以是 readonly,防止重新赋值:
溢出属性检查
在何处以及如何为对象分配类型会在类型系统中产生差异。 这方面的一个标准示例是溢出属性检查
调用 createSquare 函数时,传入的 colour 是错别字,所以报错了。
那你可能认为这不合理,因为如果想传入另一个属性 opacity 呢?也会报错,为了解决报错,可使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
但是,如果你确定该对象具有一些额外属性,则更好的方法是添加字符串索引签名。 如果 SquareConfig 可以除了上述的 color 和 width 属性,也可以具有任意数量的其他属性,那么可以这样定义:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
绕过溢出属性检查的最后一种方法(可能有点令人惊讶)是将对象分配给另一个变量: 由于分配 squareOptions 不会进行过多的属性检查,因此编译器不会报错:
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
只要你 squareOptions 和 SquareConfig 之间具有共同属性,这种方法就行得通。在上述示例中,共同的是属性 width。 但是,如果变量没有任何公共对象属性,它将报错。
对于上面这样的简单代码,建议不要试图“绕过”检查。对于更复杂的对象字面量,你需要牢记这些技巧,因为大多情况下溢出属性实际上真的是出现错误了。为了解决错误,你可以修改对象类型或传入的对象。
继承类型
有一个 BasicAddress 类型,用于描述快递信息:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
通常这些信息足够了,但可能还需要楼的单元号,那么又定义了AddressWithUnit:
interface AddressWithUnit {
name?: string;
unit: string;
//^^^^^^^^^^^^^
street: string;
city: string;
country: string;
postalCode: string;
}
这虽然达到了目的,但这只是纯粹的累加,我们必须重复 BasicAddress 中的所有其他字段。其实还有更好的方式:
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}
interface AddressWithUnit extends BasicAddress {
unit: string;
}
interface 上使用 extends 关键字允许我们从其他类型复制成员,并基于此添加想要的新成员。这减少了必须编写的类型声明样板数量。例如, AddressWithUnit 不需要重复 street等属性,并且由于 street 源自 BasicAddress ,使用者知道这两种类型在某种程度上是相关联的。
interface 还支持多种类型继承。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
interface ColorfulCircle extends Colorful, Circle {}
const cc: ColorfulCircle = {
color: "red",
radius: 42,
};
交集类型
TypeScript 还提供了另一种称为交集类型的构造,主要用于组合现有的对象类型。
交集类型是使用 & 运算符定义。
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
上面代码,我们将 Colorful 和 Circle 相交,生成一个具有 Colorful 和 Circle 的所有成员的新类型。
继承类型 vs 交集类型
上面介绍了两种类型组合方式,继承类型和交集类型,他们很相似但略有不同。两者之间的主要区别在于如何处理冲突属性。
泛型对象类型
我们有一个可以包含任何值的 Box 类型:string 、 number 、 Giraffe 等等。
interface Box {
contents: any;
}
现在, contents 属性的类型为 any ,它可以工作,但可能会导致问题。
我们可以改为使用 unknown ,但这意味着在已经知道 contents 类型的情况下,我们需要进行预防性检查,或者使用容易出错的类型断言。
interface Box {
contents: unknown;
}
let x: Box = {
contents: "hello world",
};
// we could check 'x.contents'
if (typeof x.contents === "string") {
console.log(x.contents.toLowerCase());
}
// or we could use a type assertion
console.log((x.contents as string).toLowerCase());
一种类型安全的方法是为每种类型的 contents 构建不同的 Box 类型。
interface NumberBox {
contents: number;
}
interface StringBox {
contents: string;
}
interface BooleanBox {
contents: boolean;
}
但这意味着我们必须创建不同的函数或使用函数重载:
function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
box.contents = newContents;
}
这要写很多模板代码,如果还要增加类型,就要写更多的模板代码。
幸好,我们可以使用泛型解决这个问题:
interface Box<Type> {
contents: Type;
}
Type 是一个类型变量,当我们使用 Box 时,必须给出类型参数的值。
let box: Box<string>;
这里 string 就是 Type 的值,又因为 Type 是类型变量,那么 contents 的值也是 string。也就是说, Box<string> 和我们之前的 StringBox 的工作方式相同。
Box 是可重用的,因为 Type 可以使用任何类型替换。当我们需要一个新类型的 Box 时,我们不必声明一个新的 Box 类型。
interface Box<Type> {
contents: Type;
}
interface Apple {
// ....
}
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
这也意味着我们可以通过使用泛型函数来完全避免枯燥的函数重载声明。
function setContents<Type>(box: Box<Type>, newContents: Type) {
box.contents = newContents;
}
当然,类型别名也可以是泛型的。
interface Box<Type> {
contents: Type;
}
type Box<Type> = {
contents: Type;
};
// 以上两种是相同的
由于类型别名与接口不同,它可以描述的不仅仅是对象类型,我们还可以使用它们来编写通用工具类型。
Array 类型
泛型对象类型通常是某种类型容器,其作用独立于包含的元素类型。数据结构能以这种方式工作是很好的,有助于它们跨不同的数据类型重用。
我们之前已经在使用类似的类型了:Array 类型。每当我们写出像 number[] 或 string[] 这样的类型时,实际上是 Array<number> 和 Array<string> 的简写。
Array 本身是一个泛型类型。
interface Array<Type> {
/**
* Gets or sets the length of the array.
*/
length: number;
/**
* Removes the last element from an array and returns it.
*/
pop(): Type | undefined;
/**
* Appends new elements to an array, and returns the new length of the array.
*/
push(...items: Type[]): number;
// ...
}
现代 JavaScript 还提供了其他泛型数据结构,例如 Map<K, V> 、 Set<T> 和 Promise<T> 。这些意味着,由于 Map 、 Set 和 Promise 的行为方式,它们可以与任何类型一起使用。
ReadonlyArray 类型
ReadonlyArray 是一种特殊类型,代表不应更改的数组。
就像属性的 readonly 修饰符一样,代表数据是只读的。当我们看到一个返回 ReadonlyArray 的函数时,它告诉我们不能更改数组内容,而当我们看到一个输入 ReadonlyArray 参数的函数时,它告诉我们可以将任何数组传递到该函数中,而不必担心它会被更改。
可以将常规 Array 分配给 ReadonlyArray 。
const roArray: ReadonlyArray<string> = ["red", "green", "blue"];
正如 TypeScript 为 Array<Type> 提供简写语法 Type[] 一样,它也为 ReadonlyArray<Type> 提供简写语法 readonly Type[] 。
最后要注意的一点是,与 readonly 属性修饰符不同,可分配性在常规 Array 和 ReadonlyArray 之间不是双向的。
元组类型
元组类型是另一种 Array 类型,它确切地知道包含多少个元素,以及在特定位置元素的类型。
type StringNumberPair = [string, number];
这里, StringNumberPair 是 string 和 number 的元组类型。与 ReadonlyArray 一样,它在 JavaScript 运行时没有表示形式,但对 TypeScript 很重要。对于类型系统而言, StringNumberPair 描述其 0 索引为 string 且其 1 索引为 number。
如果我们尝试访问溢出的下标,那么会报错
元组类型在高度依赖基于约定的 API 中非常有用,其中每个元素的含义都是“显而易见的”。
像这样的简单元组类型,可以使用特定索引属性的 Array 类型来模拟,但是没有长度检查报错
interface StringNumberPair {
// specialized properties
length: 2;
0: string;
1: number;
// Other 'Array<string | number>' members...
slice(start?: number, end?: number): Array<string | number>;
}
元组可以通过写出问号(在元素类型的后面加 ? )来拥有可选属性。可选元组元素只能出现在末尾,并且还会影响 length 的类型。
元组还可以有剩余元素,这些元素必须是数组/元组类型。
type StringNumberBooleans = [string, number, ...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];
StringNumberBooleans描述一个元组,其前两个元素分别为string和number,但后面可以有任意数量的boolean。StringBooleansNumber描述一个元组,其第一个元素是string,然后中间是任意数量的boolean并以number结尾。BooleansStringNumber描述一个元组,其起始元素是任意数量的boolean,并以string和number结尾。
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];
如上代码是正确的,boolean 的长度范围可以为 0 到无穷
为什么可选和剩余元素有用?它允许 TypeScript 将元组与参数列表相对应:
function readButtonInput(...args: [string, number, ...boolean[]]) {
const [name, version, ...input] = args;
// ...
}
相当于
function readButtonInput(name: string, version: number, ...input: boolean[]) {
// ...
}
当您想要使用剩余参数获取可变数量的参数,但又不想引入中间变量时,这很有用。
只读元组类型
关于元组类型的最后一个注意事项 - 元组类型有 readonly 变体,并且可以通过在它们前面添加 readonly 修饰符来指定 - 就像数组简写语法一样。
function doSomething(pair: readonly [string, number]) {
// ...
}
TypeScript 不允许写入 readonly 元组的任何属性。
大多数代码中,元组往往会被创建并且不能修改,因此尽可能将类型注释为 readonly 元组是一个推荐设置。这也很重要,并且带有 const 断言的数组文字将被推断为 readonly 元组类型。
在这里, distanceFromOrigin 并不修改内容,但需要一个可变的元组。由于 point 的类型被推断为 readonly [3, 4] ,因此它不会与 [number, number] 兼容,因为该类型不能保证 point 的元素不会发生改变。
参考:Object Types