TypeScript-常用的工具类型

334 阅读6分钟
简介

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
}

注意:

  1. 只能在类型别名定义中使用 in,如果在接口(interface)中使用,则会提示一个错误
  2. 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;