TypeScript 类型运算符
TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。
keyof 运算符
简介
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
type MyObj = {
foo: number,
bar: string,
};
type Keys = keyof MyObj; // 'foo'|'bar'
上面示例中,keyof MyObj
返回MyObj
的所有键名组成的联合类型,即'foo'|'bar'
。
下面是另一个例子。
interface T {
0: boolean;
a: string;
b(): void;
}
type KeyT = keyof T; // 0 | 'a' | 'b'
由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol
。
// string | number | symbol
type KeyT = keyof any;
对于没有自定义键名的类型使用 keyof 运算符,返回never
类型,表示不可能有这样类型的键名。
type KeyT = keyof object; // never
上面示例中,由于object
类型没有自身的属性,也就没有键名,所以keyof object
返回never
类型。
由于 keyof 返回的类型是string|number|symbol
,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。
type Capital<T extends string> = Capitalize<T>;
type MyKeys<Obj extends object> = Capital<keyof Obj>; // 报错
上面示例中,类型Capital
只接受字符串作为类型参数,传入keyof Obj
会报错,原因是这时的类型参数是string|number|symbol
,跟字符串不兼容。采用下面的交叉类型写法,就不会报错。
type MyKeys<Obj extends object> = Capital<string & keyof Obj>;
上面示例中,string & keyof Obj
等同于string & string|number|symbol
进行交集运算,最后返回string
,因此Capital<T extends string>
就不会报错了。
如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。
// 示例一
interface T {
[prop: number]: number;
}
// number
type KeyT = keyof T;
// 示例二
interface T {
[prop: string]: number;
}
// string|number
type KeyT = keyof T;
上面的示例二,keyof T
返回的类型是string|number
,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。
如果 keyof 运算符用于数组或元组类型,得到的结果可能出人意料。
type Result = keyof ['a', 'b', 'c'];
// 返回 number | "0" | "1" | "2"
// | "length" | "pop" | "push" | ···
上面示例中,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
上面示例中,Keys
是键名组成的联合类型,而MyObj[Keys]
会取出每个键名对应的键值类型,组成一个新的联合类型,即number|string
。
keyof 运算符的用途
keyof 运算符往往用于精确表达对象的属性类型。
举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。
function prop(obj, key) {
return obj[key];
}
上面这个函数添加类型,只能写成下面这样。
function prop(
obj: { [p:string]: any },
key: string
):any {
return obj[key];
}
上面的类型声明有两个问题,一是无法表示参数key
与参数obj
之间的关系,二是返回值类型只能写成any
。
有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。
function prop<Obj, K extends keyof Obj>(
obj:Obj, key:K
):Obj[K] {
return obj[key];
}
上面示例中,K extends keyof Obj
表示K
是Obj
的一个属性名,传入其他字符串会报错。返回值类型Obj[K]
就表示K
这个属性值的类型。
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; };
// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;
上面示例中,类型NewProps
是类型Obj
的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了boolean
。
下面的例子是 去掉 readonly 修饰符。
type Mutable<Obj> = {
-readonly [Prop in keyof Obj]: Obj[Prop];
};
// 用法
type MyObj = {
readonly foo: number;
}
// 等于 { foo: number; }
type NewObj = Mutable<MyObj>;
上面示例中,[Prop in keyof Obj]
是Obj
类型的所有属性名,-readonly
表示去除这些属性的只读特性。对应地,还有+readonly
的写法,表示添加只读属性设置。
下面的例子是让可选属性变成必有的属性。
type Concrete<Obj> = {
[Prop in keyof Obj]-?: Obj[Prop];
};
// 用法
type MyObj = {
foo?: number;
}
// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;
上面示例中,[Prop in keyof Obj]
后面的-?
表示去除可选属性设置。对应地,还有+?
的写法,表示添加可选属性设置。
in 运算符
JavaScript 语言中,in
运算符用来确定对象是否包含某个属性名。
const obj = { a: 123 };
if ('a' in obj)
console.log('found a');
上面示例中,in
运算符用来判断对象obj
是否包含属性a
。
in
运算符的 左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。
TypeScript 语言的类型运算中,in
运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
上面示例中,[Prop in U]
表示依次取出联合类型U
的每一个成员。
上一小节的例子也提到,[Prop in keyof Obj]
表示取出对象Obj
的每一个键名。
方括号运算符
方括号运算符([]
)用于取出对象的键值类型,比如T[K]
会返回对象T
的属性K
的类型。
type Person = {
age: number;
name: string;
alive: boolean;
};
// Age 的类型是 number
type Age = Person['age'];
上面示例中,Person['age']
返回属性age
的类型,本例是number
。
方括号的参数如果是联合类型,那么返回的也是联合类型。
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];
上面示例中,Obj
的属性名是字符串的索引类型,所以可以写成Obj[string]
,代表所有字符串属性名,返回 的就是它们的类型number
。
这个语法对于数组也适用,可以使用number
作为方括号的参数。
// MyArray 的类型是 { [key:number]: string }
const MyArray = ['a','b','c'];
// 等同于 (typeof MyArray)[number]
// 返回 string
type Person = typeof MyArray[number];
上面示例中,MyArray
是一个数组,它的类型实际上是属性名的数值索引,而typeof MyArray[number]
的typeof
运算优先级高于方括号,所以返回的是所有数值键名的键值类型string
。
注意,方括号里面不能有值的运算。
// 示例一
const key = 'age';
type Age = Person[key]; // 报错
// 示例二
type Age = Person['a' + 'g' + 'e']; // 报错
上面两个示例,方括号里面都涉及值的运算,编译时不会进行这种运算,所以会报错。
extends...?: 条件运算符
TypeScript 提供类似 JavaScript 的?:
运算符这样的三元运算符,但多出了一个extends
关键字。
条件运算符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;
上面示例中,1
是number
的子类型,所以返回true
。
下面是另外一个例子。
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;