TypesScript 类型运算
联合类型
联合类型指多个类型组成一个新的类型,用符号 |
表示:
let x: string | number;
x = 123; // 正确
x = "abc"; // 正确
2
3
4
boolean 类型就是一种联合类型,等同于 true | false
。联合类型的第一个成员前面也可以加上 |
,可以便于多行书写(编辑器格式化代码会取决于单行长度自动调整为此效果):
let x:
| 'one'
| 'two'
| 'three'
| 'four';
2
3
4
5
交叉类型
交叉类型(intersection types)指的多个类型组成的一个新类型,使用符号 &
表示。
交叉类型 A&B
表示,任何一个类型必须同时属于 A 和 B,才属于交叉类型 A&B,即交叉类型同时满足 A 和 B 的特征。
let x: number & string;
上面示例中,变量 x 同时是数值和字符串,这当然是不可能的,所以 TS 会认为 x 的类型实际是 never。
交叉类型的主要用途是表示对象的合成。
let obj: { foo: string } & { bar: string };
obj = {
foo: "hello",
bar: "world",
};
2
3
4
5
6
上面示例中,变量 obj 同时具有属性 foo 和属性 bar。
交叉类型常常用来为对象类型添加新属性。
type A = { foo: number };
type B = A & { bar: number };
2
3
上面示例中,类型 B 是一个交叉类型,用来在 A 的基础上增加了属性 bar。
in 运算符
JS 中,in 用来确定对象是否包含某个属性名。在 TS 中,in 有不同的用法,用于取出(遍历)联合类型的每一个成员类型:
type U = "a" | "b" | "c";
type Foo = {
[Prop in U]: number;
};
// 等同于
// type Foo = {
// a: number;
// b: number;
// c: number;
// };
2
3
4
5
6
7
8
9
10
11
keyof 运算符
keyof 接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型:
type MyObj = {
foo: number;
bar: string;
};
type Keys = keyof MyObj; // 'foo'|'bar'
2
3
4
5
6
由于 JS 对象的键名只有三种类型,所以对于 any
的键名的联合类型就是 string|number|symbol
:
// string | number | symbol
type KeyT = keyof any;
2
对于没有自定义键名的类型使用 keyof 运算符,返回 never
类型,表示不可能有这样类型的键名:
type KeyT = keyof object; // never
如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。但需要注意使用字符串索引时,也包括了属性值为数值的情况,因为 JS 中数值属性名会自动转为字符串:
// 示例一
interface T {
[prop: number]: number;
}
// number
type KeyT = keyof T;
// 示例二
interface T {
[prop: string]: number;
}
// string|number
type KeyT = keyof T;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于联合类型,keyof 返回成员共有的键名:
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
// 返回 'z'
type KeyT = keyof (A | B);
2
3
4
5
对于交叉类型,keyof 返回所有键名:
type A = { a: string; x: boolean };
type B = { b: string; y: number };
// 返回 'a' | 'x' | 'b' | 'y'
type KeyT = keyof (A & B);
// 相当于
keyof (A & B) ≡ keyof A | keyof B
2
3
4
5
6
7
8
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写:
type MyObj = {
foo: number;
bar: string;
};
type Keys = keyof MyObj;
type Values = MyObj[Keys]; // number|string
2
3
4
5
6
7
8
keyof 运算符往往用于精确表达对象的属性类型,例如参数表示是对象的键时,使用 keyof 可以精确的表达返回值类型:
function prop<Obj, K extends keyof Obj>(obj: Obj, key: K): Obj[K] {
return obj[key];
}
2
3
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值,详见类型映射
方括号运算符
方括号运算符([]
)用于取出对象的键值类型,比如 T[K]
会返回对象 T
的属性 K
的类型:
type Person = {
age: number;
name: string;
alive: boolean;
};
// Age 的类型是 number
type Age = Person["age"];
2
3
4
5
6
7
8
方括号的参数如果是联合类型,那么返回的也是联合类型:
type Person = {
age: number;
name: string;
alive: boolean;
};
// number|string
type T = Person["age" | "name"];
// number|string|boolean
type A = Person[keyof Person];
2
3
4
5
6
7
8
9
10
11
方括号运算符的参数也可以是属性名的索引类型:
type Obj = {
[key: string]: number;
};
// number
type T = Obj[string];
2
3
4
5
6
这个语法对于数组也适用,可以使用 number 作为方括号的参数:
// MyArray 的类型是 { [key:number]: string }
const MyArray = ["a", "b", "c"];
// 等同于 (typeof MyArray)[number]
// 返回 string
type Person = (typeof MyArray)[number];
2
3
4
5
6
如果访问不存在的属性会报错,还需要注意方括号中不能有值的运算:
// 示例一
const key = 'age';
type Age = Person[key]; // 报错
// 示例二
type Age = Person['a' + 'g' + 'e']; // 报错
2
3
4
5
6
条件运算符
TS 中提供了类似 JS 三元运算符的语法 T extends U ? X : Y
,可以根据判断 T 类型是否可以赋值给 U 类型,返回 X 或 Y 类型:
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// number
type T1 = Dog extends Animal ? number : string;
// string
type T2 = RegExp extends Animal ? number : string;
2
3
4
5
6
7
8
9
10
11
12
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。如果不希望联合类型被条件运算符展开,可以把 extends 两侧的操作数都放在方括号里面:
// 示例一
type ToArray<Type> = Type extends any ? Type[] : never;
// 相当于:(string extends any ? string[] : never) | (number extends any ? number[] : never)
type T = ToArray<string | number>; // string[]|number[]
// 示例二
type ToArray<Type> = [Type] extends [any] ? Type[] : never;
// (string | number)[]
type T = ToArray<string | number>;
2
3
4
5
6
7
8
9
10
11
TIP
将 extends 左侧的类型用方括号括起来是 TS 的特殊语法,但括起来后左侧变成了元组类型。所以对应的 extends 右侧类型也需要加上方括号变为元组类型,才能保持原本需要的类型对应关系。
条件运算符还可以嵌套使用:
type LiteralTypeName<T> = T extends undefined
? "undefined"
: T extends null
? "null"
: T extends boolean
? "boolean"
: T extends number
? "number"
: T extends bigint
? "bigint"
: T extends string
? "string"
: never;
// "bigint"
type Result1 = LiteralTypeName<123n>;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
infer 关键字
infer
关键字用来定义泛型里面推断出来的类型参数(可以理解为一个临时变量,值由 TS 从泛型中推断)。它通常跟条件运算符一起使用,用在 extends
关键字后面的父类型之中:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
2
3
4
上面的例子中,可以将 Item
理解为定义的变量,当泛型 Type
传入并且满足数组类型(也就是 Array<xxx>
)时,Item
会替换数组类型中的泛型(前面的 xxx
),所以 Item
被推断为数组的成员类型。
如果不用 infer
定义类型参数,就需要传入两个类型参数,比较麻烦:
type Flatten<Type, Item> = Type extends Array<Item> ? Item : Type;
下面是推断函数的参数类型和返回值类型的例子:
type ReturnPromise<T> = T extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T;
2
3
下面是提取对象指定属性的例子:
type MyType<T> = T extends {
a: infer M;
b: infer N;
}
? [M, N]
: never;
// 用法示例
type T = MyType<{ a: string; b: number }>; // [string, number]
2
3
4
5
6
7
8
9
下面是正则匹配提取类型参数的例子:
type Str = "foo-bar";
type Bar = Str extends `foo-${infer rest}` ? rest : never; // 'bar'
2
3
模板字符串
TS 允许使用模板字符串构建类型。模板字符串可以引用的类型一共 6 种,分别是 string、number、bigint、boolean、null、undefined。引用这 6 种以外的类型会报错:
type World = "world";
// "hello world"
type Greeting = `hello ${World}`;
2
3
4
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型:
type T = "A" | "B";
// "A_id"|"B_id"
type U = `${T}_id`;
2
3
4
如果模板字符串引用两个联合类型,它会交叉展开这两个类型:
type T = "A" | "B";
type U = "1" | "2";
// 'A1'|'A2'|'B1'|'B2'
type V = `${T}${U}`;
2
3
4
5
6
satisfies 运算符
有时我们希望确保某个表达式匹配某个类型,但也希望保留 TS 对这个表达式的类型推断。TS 4.9 中添加了 satisfies
(译为满足)运算符用来满足这个需求:
type T = {
url: string | string[];
};
const a: T = {
url: "www.xxx.com",
};
a.url.toUpperCase(); // 报错,string[] 类型没有 toUpperCase 方法
2
3
4
5
6
7
8
9
上面的例子中虽然从上下文可以确定 a.url
是字符串类型,但因为我们指定了类型为 T
,所以 TS 便不会再自动推断属性 a
的类型。此时就可以使用 satisfies
运算符:
type T = {
url: string | string[];
};
const a = {
// url 推断为 string 类型
url: "www.xxx.com",
} satisfies T; // 限制了 a 需要满足 T 类型,如果赋值不满足 T 类型会报错
// 此时可以正确调用字符串方法
a.url.toUpperCase();
2
3
4
5
6
7
8
9
10
11