简介
TypeScript中常用的工具类型,包括extends、keyof、in、typeof、Record、Partial、Required、Readonly、Pick、Omit、Exclude、Extract、NonNullable。
extends
extends关键字在TS编程中出现的频率很高,而且不同场景下代表的含义是不一样的:
- 表示类型组合的含义
- 表示继承的含义
- 表示泛型约束的含义
类型组合
extends 可以跟 interface 结合起来使用,用于表达类型组合。interface的 extends 从句可以跟着多个组合对象,多个组合对象之间用逗号:
interface NameInfo {
name: string;
}
type AgeInfo = {
age: number;
}
class Address {
constructor(address: string) {
this.address = address;
}
address: string
}
interface PersonInfo extends NameInfo, AgeInfo, Address {
height: number;
}
let zs: PersonInfo = {
name: "张三",
age: 20,
height: 177,
address: 'address',
};
继承
当extends用于 typeScript 的类之间,它的准确语义也就是 ES6 中面向对象中「extends」关键字的语义。
class Person {
public readonly name: string = '张三';
protected age: number = 20;
private height: string = '180';
protected getPersonInfo(): void {
console.log(this.name, this.age, this.height); // 基类里面三个修饰符都可以访问
}
}
class Male extends Person {
public getInfo(): void {
console.log(this.name, this.age); // 子类只能访问public、protected修饰符的
}
}
let m = new Male();
console.log(m.name); // 类外部只能访问public修饰的
m.name = '李四'; // name属性使用只读修饰符,所以不能对name进行赋值修改操作
另一个例子:
这个例子会报错!
class Person {
private name: string = '张三';
age: number = 20;
getName(): string {
return this.name;
}
}
interface NewPersion extends Person {
height: string;
}
class Man implements NewPersion { // 报错
private name: string = '张三';
age: number = 20;
height: string = '180';
getName(): string {
return this.name;
}
}
// 因为NewPersion接口组合了类Person的约束,所以当Man去实现接口NewPersion时,就要遵循相关约束
如果某个 interface A 继承了某个 class B,那么这个 interface A 还是能够被其他 interface 去继承(或者说组合)。但是,如果某个 class 想要 implements 这个 interface A,那么这个 class 只能是 class B 本身或者 class B 的子类。
所以,改正上面的例子:
class Person {
private name: string = '张三';
age: number = 20;
getName(): string {
return this.name;
}
}
interface NewPersion extends Person {
height: string;
}
class Man extends Person implements NewPersion {
// private name: string = '张三';
age: number = 20;
height: string = '180';
// getName(): string {
// return this.name;
// }
}
let man = new Man();
console.log(man.getName()) // 张三
注意:
- 遵循 ES6 中 extends关键字一样的语义,ts 中的 extends 也是不能在同一时间去继承多个父类的。
泛型约束
当 extends 跟泛型形参结合的时候,表达的是「类型约束」语义,也就是凡是有泛型形参的地方,都可以通过 extends 来表达类型约束。
表达方式:泛型形参T
extends 某个类型U
先来做一做题:
// case 1
type UselessType<T extends number> = T;
// 不会报错,因为any可以赋值给任何类型,不会对any进行类型检查,也就可以理解any满足任何类型的约束
type Test1 = UselessType<any>
// 报错,因为 number | string 约束不能满足 number 约束
// 其实内部是有一个分配律原则,即 number | string extends number 等效于:
// (number extends number) | (string extends number)
// 所以,并不能把类型string约束分配给类型number约束
type Test1_1 = UselessType<number | string>
// case 2
type UselessType2<T extends { a: 1, b: 2 }> = T;
// 不会报错,因为{ a: 1, b: 2, c: 3 }约束兼容{ a: 1, b: 2 }约束
type Test2 = UselessType2<{ a: 1, b: 2, c: 3 }>
// 报错,因为{ a: 1 }约束不能兼容{ a: 1, b: 2 }约束,前者缺少b的约束条件,所以两者不兼容
type Test2_1 = UselessType2<{ a: 1 }>
// 报错,同样还是约束不兼容
// { [key: string]: any } 代表的是一个任意对象
// 所以{ [key: string]: any }约束,并不一定满足{ a: 1, b: 2 }约束
type Test2_2 = UselessType2<{ [key: string]: any }> // 这里会报错吗?
// 结果是true
type Test2_3 = { a: 1, b: 2 } extends { [key: string]: any } ? true : false
// case 3
class BaseClass {
name!: string
}
class SubClass extends BaseClass {
sayHello!: (name: string) => void
}
class SubClass2 extends SubClass {
logName!: () => void
}
type UselessType3<T extends SubClass> = T;
// 报错,因为{ name: '鲨叔' }缺少sayHello的约束定义
type Test3 = UselessType3<{ name: '鲨叔' }> // 这里会报错吗?
// 不会报错,自身与自身约束肯定互相兼容、互相满足
type Test3_1 = UselessType3<SubClass> // 这里会报错吗?
// 报错,因为BaseClass是父类约束,缺少子类定义的sayHello约束
type Test3_2 = UselessType3<BaseClass> // 这里会报错吗?
通过上面的例题可以发现,A extends B满足的前提是,A与B类型完全相同,或者是B类型的一切约束条件,A都具备。需要理解的是类型A可以分配给类型B,并不代表类型A就是类型B的子集,需要判断的是类型A是否满足类型B的约束条件。
同样,还可以结合三元运算符对类型做判断:
type TypeFn<P> = P extends string | number ? P[] : P;
let m: TypeFn<number> = [1, 2, 3];
let m1: TypeFn<string> = ['1', '2', '3'];
let m2: TypeFn<boolean> = true;
类型推断 infer
类型推断infer相当于声明一个变量接收传入的类型
type ObjType<T> = T extends { name: infer N; age: infer A } ? [N, A] : [T];
let p: ObjType<{ name: string; age: number }> = ["张三", 1];
let p1: ObjType<{name: string}> = [{name: '张三'}];
keyof
keyof提取对象属性名、索引名、索引签名的类型;
interface Person{
name:string;
age:number;
[idx:number]:number|string;
[idx:string]:number|string;
}
// keyof 后面一般跟接口, 表示接口的这些属性名之一 (实际上就是 ":" 前面的这些)
type Ptype = keyof Person; // "name" | "age" | number | string
let p1:Ptype;
p1 = "name"
p1 = "age"
p1 = 1
p1 = "123"
in
in是映射类型
type NumAndStr = number | string;
type TargetType = {
[key in NumAndStr]: string | number;
};
let obj: TargetType = {
1: '123',
"name": 123
}
注意:
- 只能在类型别名定义中使用 in,如果在接口(interface)中使用,则会提示一个错误
- in 和 keyof 也只能在类型别名定义中组合使用
typeof
typeof 的主要用途是在类型上下文中获取变量或者属性的类型
// 推断变量的类型
let strA = "2";
type KeyOfType = typeof strA; // string
// 反推出对象的类型作为新的类型
let person = {
name: '张三',
getName(name: string):void {
console.log(name);
}
}
type Person = typeof person;
Record
用法:Record<K, T>
作用是创建一个对象类型。Record的内部定义是接收两个泛型参数,Record后面的泛型就是对象键和值的类型。
Record<string, never> // 代表空对象
Record<string, unknown> // 代表任意对象
Partial
用法:Partial
作用是生成一个新的类型,新类型与 T 拥有相同的属性,但是所有属性皆为可选项
interface Person {
name: string;
age: number;
}
type NewPerson = Partial<Person>
let person: Person = {
name: '人类',
age: 18
}
let nPerson: NewPerson = {
name: '新人类'
}
Required
用法:Required
作用是生成一个新类型,该类型与 T 拥有相同的属性,但是所有属性皆为必选项
interface Person {
name?: string;
age?: number;
}
type NewPerson = Required<Person>
let person: Person = {
name: '人类',
// age: 18
}
let nPerson: NewPerson = {
name: '新人类',
age: 18
}
Readonly
用法:Readonly
作用是生成一个新类型,该类型与 T 拥有相同的属性,但是所有属性皆为只读
interface Person {
name?: string;
age?: number;
}
type NewPerson = Readonly<Person>
let nPerson: NewPerson = {
name: '新人类',
age: 18
}
nPerson.age = 19; // 报错
Pick
用法:Pick<T, K>
作用是生成一个新类型,该类型拥有 T 中的 K 属性集 ; 新类型相当于T与K的交集
interface Person {
name: string;
age: number;
address: string
}
type NewPerson = Pick<Person, 'name'| 'age'>
let nPerson: NewPerson = {
name: '新人类',
age: 18
}
Omit
用法:Omit<T, K>
与Pick相反,从T中选取所有的属性值,然后移除属性名在K中的属性值
interface Person {
name: string;
age: number;
address: string
}
type NewPerson = Omit<Person, 'name'| 'age'>
let nPerson: NewPerson = {
address: 'address'
}
Exclude
用法:Exclude<T, U>
作用是从联合类型T中去掉所有能够赋值给U的属性,然后剩下的属性构成一个新的类型
type T0 = Exclude<"a" | "b", "a">;
// 等价于:
type T0 = Exclude<"a", "a"> | Exclude<"b", "a">
// 等价于:
type T0 = ("a" extends "a" ? never : "a") | ("b" extends "a" ? never : "b")
// 等价于:
type T0 = never | "b"
= "b"
// 内部是应用来分配律,即把联合类型单独项拆开,分开比较
Extract
用法:Extract<T, U>
Extract和上面的Exclude刚好相反,它是将第二个参数的联合项从第一个参数的联合项中提取出来
type T0 = Extract<"a" | "b", "a">
= "a"
NonNullable
用法:NonNullable
去除 null 和 undefined 后的新类型
type T0 = number | null | undefined;
type T1 = NonNullable<T0>
let t: T1 = null; // 报错
t = undefined; // 报错
t = 1;