类型编程: 打造属于自己的工具类型

191 阅读8分钟

类型物料

泛型

工具类型的本质就是构造复杂类型的泛型。

type isXX = 1 extends number ? true : false; 
type isYY = 'string' extends string ? true : false;

这是使用了extends关键字和type类型别名返回了布尔字面量true和false, 这是一种效率低下的做法, 因为不能把其中的逻辑复用在对其他类型子类型关系的判断上。 此时,我们需要吧确切的类型抽离为入参, 然后封装成一个可复用的泛型。

type isSubTyping<Child, Parent> = Child extends Parent ? true : false;
type isXX2 = isSubTyping<1, number>; // true
type isYY2 = isSubTyping<'string', number>; // false
type isZZ2 = isSubTyping<true, boolean>; // true

条件类型

TypeScript支持使用三元运算条件类型, 它可以根据 ? 前面的条件判断返回不同的类型。三元运算支持嵌套。

三元运算主要使用extends关键字帕努单两个类型的子类型关系。

type isSubTyping<Child, Par> = Child extends Par ? true : false;
type isAssertable<T, S> = T extends S ? true : S extends T ? true : false;
type isNumAssertable = isAssertable<1, number>; // true
type isStrAssertable = isAssertable<string, 'string'>; // true
type isNotAssertable = isAssertable<1, boolean>; // false

利用extends关键字判断入参T是否是S的子类型或S是T的子类型, 从而判断它们之间是否可断言关系。

分配条件类型 Distributive Conditional Types

条件类型中, 如果入参时联合类型, 则会被拆解为一个个独立的(原子)类型(成员),然后再进行类型运算。

type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; // string[] | boolean;
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; // string | boolean;  

string和boolean组成的联合类型BooleanOrString作为泛型StringOrNumberArray入参时, 会被拆解成string和boolean两个独立的类型, 再通过extends关键字判断是否是string|number类型的子类型。

可以使用[]包括传参来强制类型传参被当成一个整体,解除类型分配。

type StringOrNumberArray<E> = [E] extends [string | number]: E[] : E;
type WhatIsThis = StringOrNumberArray<string | boolean>; // string | boolean;

never 陷阱

注意, 包含条件类型的泛型接收never作为泛型入参时,存在一定“陷阱”, 如下:

type GetSNums = never extends number 
                    ? number[] 
                    : never extends string 
                        ?  string[]
                        : never; // number[]

type GetNever = StringOrNumberArray<never>; // never

因为never是所有类型的子类型, 自然也是number的子类型,所以返回的是number类型的数组;第二行传入never作为入参来实例化前面定义的典型StringOrNumberArray时,返回的类型却是never.

泛型StringOrNumberArray的实现与示例第一行=右侧的逻辑并没有任何区别(除never被抽离成入参之外), 这是因为never是不能分配的底层类型, 如果 作为入参 以原子形式出现在条件判断extends关键字左侧,则实例化得到的类型也是never

type UsefulNeverX<T> = T extends {} ? T[] : [];
type uselessNeverX<T, S> = S extends {} ? S[] : [];
type UselessNeverY<T, S> = S extends {} ? T[] : [];
type UselessNeverZ<T> = [T] extends [{}] ? T[][];

type ThisIsNeverX = UsefulNever<never>; // never
type ThisIsNotNeverX = UselessNeverX<never, string>; // string[];
type ThisIsNotNeverY = UselessNeverY<never, string>; // never[];
type ThisIsNotNeverZ = UselessNeverZ<bever>; // never[]

泛型UsefulNeverX的入参T被三元运算中的extends使用,所以UsefulNeverX<never>返回never. 后面两个因为没有用到入参T, 所以返回never[], 最后一行入参T是以T[] 而不是以原型形式被extends使用, 所以返回never[]。 综上,never作为泛型传参, 返回never的前提条件:

  • 作为泛型入参
  • 原子形式被使用(出现在extends左侧, 独立使用, T[]不属于原子形式)

条件类型中的类型推断infer

我们可以在条件类型中使用类型推断操作符infer来获取类型入参的组成部分, 比如获取数组类型入参里的元素类型。 TODO?????

type ElementTypeOfArray<T> = T extends (infer E)[] ? E[] : never;
type isNumber = ElementTypeOfArray<number[]>; // number
type isNever = ElementTypeOfArray<number>; // never

示例中, 我们定义了接收入参T的泛型ElementTypeOfArray, 并在三元运算符的条件判断中, 通过(infer E)[]定义了一个有对元素类型推断参数E的数组, 当入参T满足是(infer E)[]数组类型的子类型的条件, 则返回参数E,及数组元素类型, 所以传入number[] 返回number, 传入number返回never。

我们还可以通过infer创建人一个类型推断参数, 以此获取任意的成员类型。

type ElementTypeOfObj<T> = T extends {name: infer E, id: infer I} ? [E, I] : never;
type isArray = ElementTypeOfObj<{name: 'name', id: 1}>; // ['name', 1]
type isNever = ElementTypeOfObj<number>; // never 

定义了入参是 T 的泛型 ElementTypeOfObj,并通过两个 infer 类型推断来获取入参 name、id 属性的类型。在第 3 行,因为入参是包含 name、id 属性的接口类型,所以提取到了元组类型 ['name', 1]。而在第 4 行,因为入参 number 不满足三元运算中的条件判断,所以返回了 never。

索引访问类型

索引访问类型其实更像是获取物料的方式,首先我们可以通过属性名,索引,索引签名按需提取对象(接口类型)任意成员的类型(注意:只能使用【索引名】的语法)。如下:

inferface MixedObject {
    animal: {
        type: 'animal' | 'dig' | 'cat',
        age: number
    };
    // 属性约束 key为number类型的值取值
    [name: number]: {
        type: string;
        age: number;
        nickname: string;
    };
    // 属性约束, key为string类型的值取值类型
    [name: string]: {
        type: string;
        age: number
    }
}

type animal = MixedObject['animal'];
type animalType = MixedObject['animal']['type']
// 等效
type numberIndex = MixedObject[number]
type numberIndex0 = MixedObject[0];

// 等效
type stringIndex = MixedObject[string];
type stringIndex0 = MixedObject['string'];

传送门: 接口类型-索引签名

keyof

可以使用keyof关键字提取对象属性名,索引名,索引签名的类型。 返回联合类型

type MixedObjectKeys = keyof MixedObject; // string | number
type animalKeys = keyof animal; // 'type' | 'age'
type numberIndexKeys = keyof numberIndex; // 'type' | 'age' | 'nickname'

typeof

在表达式上下文中使用typeof,是用来获取表达式值的类型, 如果在类型上下文中使用,则是用来获取变量或者属性的类型。 在TypeScript中, typeof的主要用途是在类型上下文中获取变量或者属性的类型

let StrA = 'a'
// 表达式中使用
const unions = typeof StrA;  // "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"

// 类型上下文中使用
const str: type StrA = '123' // 'string'
// 使用类型别名提取类型
type DerivedFromStrA = typeof StrA; // 'string'

注意区分表达式上下文(js场景,获取的是变量值的类型,所以是'string' | 'number' | ... 组成的联合类型)和类型上下文(ts环境,获取的是变量的类型)

对于任何未显示添加类型注解或值与类型注解一体(比如函数,类)的变量或者属性, 我们可以使用typeof提取他们的类型。

const animal = {
    id: 1,
    name: 'animal'
}

type Animal = typeof animal;
const animalFun = () => animal
type AnimalFun = typeof animalFun;

映射类型

我们可以使用索引签名语法和in关键字限定对象属性的范围。 如:

type SpecifiedKeys = 'id' | 'name';
type TargetType = {
    [key in SpecifiedKeys]: unknown;
}

type TargetGeneric<O extends string | number | symbok> = {
    [key in O]: unknown;
}

type TargetInstance = TargetGeneric<SpecifiedKeys>; // {id: unknown; name: unknown}

in,keyof 只能在类型别名中使用,在接口中使用一般使用具体的类型

interface SourceInterface {
    readonly id: number;
    name?: string; // 可缺省
}

type TargetType = {
    [key in keyof SourceInterface]: SourceInterface[key]
}

type TargetGenericType<S> = {
    [key in keyof S]: S[key]
}
type TargetInstance = TargetGeneric<SourceInterface>;
// {readonly id: number; name?: string | undefined }

同样, 可以在映射类型中使用readonly?修饰符来描述属性的可读性,可选性。也可以在修饰符前添加+-前缀表示添加移除指定修饰符。

type TargetGenericTypeReadonly<S> = {
    readonly [key in keyof S]: S[key]
}
// {readonly id: number; readonly name?: string}
type TargetGenericTypeReadonlyInstance = TargetGenericTypeReadonly<SourceInterface>;

type TargetGenericTypeOptional<S> = {
    [K in keyof S]?: S[K]
}
type TargetGenericTypeOptionalInstance = TargetGenericTypeOptional<SourceInterface>;
// {readonly id?: number; name?: string | undefined; }

type TargetGenericTypeRemoveReadonly<S> = {
    -readonly [K in keyof S]: S[K]
}
type TargetGenericTypeRemoveReadonlyInstace = TargetGenericTypeRemoveReadonly<SourceInterface>;
// {readonly id: number; readonly name?: string | undefined;}


type TargetGenericTypeRemoveOptional<S> = {
   [K in keyof S]-?: S[K]
}
type TargetGenericTypeRemoveOptionalInstance = TargetGenericTypeRemoveOpional<SourceInterface>;
// {readonly id: number; name: string}

使用as重新映射key, 自TypeScript 4.1起,我们可以在映射类型的索引签名中使用类型断言。

type TargetGenericTypeAssertion<S> = {
    [K in keyof S as Exclude<K, 'id'>]: S[K]
}

type TargetGenericTypeAssertionInstance = TargetGenericTypeAssertion<SourceInterface>;
// {name?: string | undefined;}

造轮子

Exclude

先看看Exclude使用实例

type ExcludeSpecifiedNumber = Exclude<1 | 2, 1>; // 2
type ExcludeSpecifiedString = Exclude<'id' | 'name', 'id'>; // name
type ExcludeSpecifiedBoolean = Exclude<boolean, true>; // false

再来看看Exclude实现源码

type Exclude<T, U> = T extends U : never : T;

这里就是利用了在类型别名中使用泛型传参的分配条件类型特性,入参T会被拆解成员类型。 如果成员类型时入参T的子类型, 则返回never,否则返回成员类型。

ReturnTypeOfResolved

ReturnType: 返回函数的返回类型 ReturnTypeOfResolved: 如果入参F的返回类型时泛型Promise的实例, 则返回Promise接收的入参。

type Reutrn<F extends (...args) => any> = F extends (...args: any[]) => infer T ? T : any;

type ReturnTypeOfResolved<F extends (...args: any) => any> = F extends (...args: any[]) => Promise<infer R> ? R : ReturnType<F>;
type isNumber = ReturnTypeOfResolved<() => number>; // number
type isString = ReturnTypeOfResolved(() => Promise<string>; // string

Merge

合并两个类型,如果有相同名称属性,但是类型不同,则进行类型联合

type Merge<A, B> = {
    [key in keyof A | keyof B]: 
        key extends keyofA
            ? key extends keyofB
                ? A[key] | B[key]
                : A[key]
            : key extends keyof[B]
                ? B[key]
                : never;
}

type merged = Merge<{id: number; name: string}, {id: string; age: number}>;
// {id: number | string; name: string; age: number;}

Equal

实现工具类型Equal<S, T>,它可以判断入参S和T是否是相同类型, 如果相同,则返回布什尔字面量true, 否则返回false

需要注意点:

  1. never类型陷阱(使用[]解除条件分配和类型陷阱)
  2. any既是所有类型的子类型, 也是所有类型的父类型(使用isAny函数来判断是否是any)
type IsAny<T> = 0 extends (1 & T) ? true : false;

type isEqual<S, T> = isEqual<S> extends true
    ? isEequal<T> extends true
        ? true
        : false
     isEqual<T> extends true
         ? false
         : [S] extends [T]
             ? [T] extends [S]
                 ? true
                 : false
             : false;
         

知识点: 只有any满足与任何类型交叉得到的都是any,而any既是所有类型的父类型, 也是所有类型的子类型。

传送门