TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。
keyof 运算符
简介
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。
// string | number | symbol
type KeyT = keyof any;
对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。
type KeyT = keyof object; // never
由于 keyof 返回的类型是string|number|symbol,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。
type Capital<T extends string> = Capitalize<T>;
type MyKeys<Obj extends object> = Capital<string & keyof Obj>;
如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。
// 示例一
interface T {
[prop: number]: number;
}
// number
type KeyT = keyof T;
// 示例二
interface T {
[prop: string]: number;
}
// string|number
type KeyT = keyof T;
如果 keyof 运算符用于数组或元组类型返回数组的所有键名,包括数字键名和继承的键名。 对于联合类型,keyof 返回成员共有的键名。
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
// 返回 'z'
type KeyT = keyof (A | B);
对于交叉类型,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
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型
type MyObj = {
foo: number;
bar: string;
};
type Keys = keyof MyObj;
type Values = MyObj[Keys]; // number|string
keyof 运算符的用途
keyof 运算符往往用于精确表达对象的属性类型
function prop(obj, key) {
return obj[key];
}
function prop<Obj, K extends keyof Obj>(
obj:Obj, key:K
):Obj[K] {
return obj[key];
}
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; };
// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;
去掉 readonly 修饰符
type Mutable<Obj> = {
-readonly [Prop in keyof Obj]: Obj[Prop];
};
// 用法
type MyObj = {
readonly foo: number;
}
// 等于 { foo: number; }
type NewObj = Mutable<MyObj>;
让可选属性变成必有的属性
type Concrete<Obj> = {
[Prop in keyof Obj]-?: Obj[Prop];
};
// 用法
type MyObj = {
foo?: number;
}
// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;
in 运算符
用来取出(遍历)联合类型的每一个成员类型。
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
方括号运算符
方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。
type Person = {
age: number;
name: string;
alive: boolean;
};
// Age 的类型是 number
type Age = Person['age'];
方括号的参数如果是联合类型,那么返回的也是联合类型。
type Person = {
age: number;
name: string;
alive: boolean;
};
// number|string
type T = Person['age'|'name'];
// number|string|boolean
type A = Person[keyof Person];
如果访问不存在的属性,会报错。
type T = Person['notExisted']; // 报错
方括号运算符的参数也可以是属性名的索引类型。
type Obj = {
[key:string]: number,
};
// number
type T = Obj[string];
方括号里面不能有值的运算。
// 示例一
const key = 'age';
type Age = Person[key]; // 报错
// 示例二
type Age = Person['a' + 'g' + 'e']; // 报错
extends...?: 条件运算符
根据当前类型是否符合某种条件,返回不同的类型。
T extends U ? X : Y
上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。
如果T能够赋值给类型U,表达式的结果为类型X,否则结果为类型Y。
// true
type T = 1 extends number ? true : false;
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。
(A|B) extends U ? X : Y
// 等同于
(A extends U ? X : Y) |
(B extends U ? X : Y)
如果不希望联合类型被条件运算符展开,可以把extends两侧的操作数都放在方括号里面。
// 示例一
type ToArray<Type> =
Type extends any ? Type[] : never;
// string[]|number[]
type T = ToArray<string|number>;
// 示例二
type ToArray<Type> =
[Type] extends [any] ? Type[] : never;
// (string | number)[]
type T = ToArray<string|number>;
条件运算符还可以嵌套使用。
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>;
// "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
infer 关键字
infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。
type Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;
上面示例中,infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten<Type>则表示Type这个类型参数是外部传入的。Type extends Array<infer Item>则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item,即Item是从Type推断出来的。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
// string
type Str = Flatten<string[]>;
// number
type Num = Flatten<number>;
如果不用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;
如果T是函数,就返回这个函数的 Promise 版本,否则原样返回。infer A表示该函数的参数类型为A,infer R表示该函数的返回值类型为R。
提取对象指定属性
type MyType<T> =
T extends {
a: infer M,
b: infer N
} ? [M, N] : never;
// 用法示例
type T = MyType<{ a: string; b: number }>;
// [string, number]
通过正则匹配提取类型参数
type Str = "foo-bar";
type Bar = Str extends `foo-${infer rest}` ? rest : never; // 'bar'
is 运算符
函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。
is运算符用来描述返回值属于true还是false。
is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。
function isFish(
pet: Fish|Bird
):pet is Fish {
return (pet as Fish).swim !== undefined;
}
is运算符可以用于类型保护。
function isCat(a:any): a is Cat {
return a.name === 'kitty';
}
let x:Cat|Dog;
if (isCat(x)) {
x.meow(); // 正确,因为 x 肯定是 Cat 类型
}
function isCat(a:any): a is Cat {
return a.name === 'kitty';
}
let x:Cat|Dog;
if (isCat(x)) {
x.meow(); // 正确,因为 x 肯定是 Cat 类型
}
上面示例中,函数isCat()的返回类型是a is Cat,它是一个布尔值。后面的if语句就用这个返回值进行判断,从而起到类型保护的作用,确保x是 Cat 类型,从而x.meow()不会报错(假定Cat类型拥有meow()方法)。
用在类(class)的内部,描述类的方法的返回值。
class Teacher {
isStudent():this is Student {
return false;
}
}
class Student {
isStudent():this is Student {
return true;
}
}
模板字符串
TypeScript 允许使用模板字符串,构建类型。
模板字符串的最大特点,就是内部可以引用其他类型。
type World = "world";
// "hello world"
type Greeting = `hello ${World}`;
模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined。
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。
type T = 'A'|'B';
// "A_id"|"B_id"
type U = `${T}_id`;
如果模板字符串引用两个联合类型,它会交叉展开这两个类型。
type T = 'A'|'B';
type U = '1'|'2';
// 'A1'|'A2'|'B1'|'B2'
type V = `${T}${U}`;