面向Type编程 -- Typescript类型和类型操作(二)

156 阅读7分钟

简述

前面介绍了typeof和keyof操作符,下面继续讨论Typescript的类型操作

三、in操作符

in 操作符用于遍历目标类型的属性key值。类似 for .. in 的机制。一般结合[]一起使用。

1. 遍历枚举类型(enum)

enum E1 {
    A,
    B,
    C
}

type TE = {
    [P in E1]: string
}

/**
 * type TE = {
    0: string;
    1: string;
    2: string;
}
 * /

2. 遍历联合类型

in操作符还可以用来遍历联合类型,生成符合目标的映射类型

type Property = 'name' | 'age' | 'phoneNum';

type PropertyMap = {
    [key in Property]: string;
}

/**
 * type PropertyMap = {
    name: string;
    age: string;
    phoneNum: string;
}
 */

3. 可以结合keyof一起使用

前面讲过,keyof可以获取类型key值的联合类型,结合in操作符可以创建映射类型

let colors = {
    red: 'Red',
    green:'Green',
    blue:'Blue'
}

type Colors = typeof colors

type Partial<T> = {
    [P in keyof T]?: T[P]
}

type ColorMap1 = Partial<Colors>

/* 可以得到
 * type ColorMap1 = {
    red?: string;
    green?: string;
    blue?: string;
   }
*/

4. 结合基础类型使用

in 操作符还可以用于基础类型(string, number, symbol)

type StringKey = {
    [key in string]: any;
}

/**
 * type StringKey = {
 *     [x: string]: any;
 * }
 */

type NumberKey =  {
    [key in number]: any;
}

/**
 * type NumberKey = {
 *     [x: number]: any;
 * }
 */

in操作符结合[]一起使用,可以进行遍历类型操作,创建新的类型。这是面向Type编程重要基础。

四、 extends操作符

1. extends操作符可以用类于或者类型继承

class Animal {
    name: string;
}

class Dog extends Animal {
    breed: string;
}

interface Person {
    name: string;
    age: number;
}

interface Player extends Person {
    item: 'ball' | 'swing';
}

keyof Player // 'name' | 'age' | 'item'

2. extends可以用作类型范围限制

extends操作符结合泛型使用时,可以用作类型范围限制。例如前面讨论keyof操作符,声明的getProp函数。这里限制了K的取值范围必须是keyof T,也即类型T的key值的联合类型或者其子集

function getProp<T, K extends keyof T>(obj: T, key: K) {   
    return obj[key]; 
}

另外,Typescript工具库内置了很多类型操作,比如Pick筛选,就用到了extends操作符做类型限制

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
注:lib.es5.d.ts中内置了很多有用的类型操作工具,我们将在后面章节详细讨论

3. extends操作符用于条件类型

typescript 2.8引入了条件类型表达式,类似于三元运算符。格式如下例,表示如果T包含的类型是U包含的类型的 ‘子集’,那么取结果X,否则取结果Y。

T extends U ? X : Y

某种意义上,extends为类型操作提供了条件判断能力,这很重要。

type NonNullable<T> = T extends null | undefined ? never : T;  // 如果泛型参数 T 为 null 或 undefined,那么取 never,否则直接返回T。
let demo1: NonNullable<number>; // => number,因为number不是null | undefined的子集
let demo3: NonNullable<undefined>; // => never,因为undefine是null | undefined的子集

4. 条件类型支持嵌套

类似于三元运算符,条件类型支持嵌套,按照顺序依次执行

type TypeName<T> =
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;  // "string"
type T1 = TypeName<"a">;  // "string"
type T2 = TypeName<true>;  // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;  // "object"

5. 分布式条件类型

在条件类型T extends U ? X : Y 中,当泛型参数 T 取值为 A | B | C 时,且A\B\C均为裸类型时,这个条件类型可以拆解为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y),这就是分布式条件类型

裸类型表示没有被包裹(Wrapped) 的类型,(如:Array、[T]、Promise 等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。\

例如:

type T10 = TypeName<string | (() => void)>;  // "string" | "function"
type T12 = TypeName<string | string[] | undefined>;  // "string" | "object" | "undefined"
type T11 = TypeName<string[] | number[]>;  // "object"

四、infer操作符

1. infer操作符可以用来声明一个待推断的类型变量

条件类型表达式中,可以使用infer关键字来声明一个待推断的类型变量。且infer只能结合extends在条件类型中使用。例如,内置工具类型ReturnType,作用是用来推断函数的返回值类型:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
  • extends限制泛型输入必须是函数类型
  • 如果满足条件,infer将函数返回结果存储到一个类型变量R当中,否则返回通用类型any

很显然,这里巧妙的利用了Typescript推断类型机制。最终实现推断函数返回类型的目的。看下面实例,会更加直观

let add = (a: number, b: number) => a + b
type RT = ReturnType<typeof add>
type T0 = ReturnType<() => string>  // string
type T1 = ReturnType<(s: string) => void> // void
type T2 = ReturnType<<T>() => T>  // unkonwn

当然,infer同样可以用在条件类型嵌套里面

type Unpacked<T> =
    T extends (infer U)[] ? U :
    T extends (...args: any[]) => infer U ? U :
    T extends Promise<infer U> ? U :
    T;

type T0 = Unpacked<string>;  // string
type T1 = Unpacked<string[]>;  // string
type T2 = Unpacked<() => string>;  // string
type T3 = Unpacked<Promise<string>>;  // string
type T4 = Unpacked<Promise<string>[]>;  // Promise<string>
type T5 = Unpacked<Unpacked<Promise<string>[]>>;  // string

2. infer可以用在条件类型不同声明类型的位置

上面例子中,infer分别用在几个不同的位置。说明只要满足条件类型,infer可以用在不同声明类型的位置,或者推断类型的位置。例如,前面讲过的infer可以返回值的位置上,当然还可以用在参数、泛型声明、属性类型等位置

  • 数组类型推断
type ArrayType<T extends any[]> = T extends (infer U)[] ? U : any

type A1 = ArrayType<string[]> // string

let arr = [1,2,3]
type A2 = ArrayType<typeof arr> // number
  • Promise类型推断
type PromiseType<T extends Promise<any>> = T extends Promise<infer U> ? U : any

type P1 = PromiseType<Promise<string>> // string
type P2 = PromiseType<Promise<number>> // string
  • 参数类型推断
type ParamsType<T extends (...args:any)=>any>  = T extends (...args: infer U) => any ? U : never

let add = (a: number, b: number) => a + b

type P1 = ParamsType<typeof add> // [a:number, b:number]

3. 借助infer实现类型转换

基于infer可以声明待推断类型,可以实现类型转换,例如联合类型(Union)、交叉类型(Intersection)、元组(Tuple)之间的相互转换

  • 元组转联合类型

元组其实就是元素类型不同的数组。这样描述,虽然有些笼统,但有助于我们理解和使用元组。在一定条件下,可以把元组当做数组的子集,同时可以推断Tuple extends Array表达式结果为真。利用这个特性,infer可以声明并存储一个推断类型,以此达到元组转联合类型的目的

type TupleToUnion<T> = T extends (infer U)[] ? U : never

type T1 = [string, number]
type U1 = TupleToUnion<T0> // string | numbert
  • 联合类型转交叉类型

下面是一个联合类型转交叉类型的泛型工具。我们一起分析一下实现原理,从而加深对infer操作符的理解。

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type TAB = UnionToIntersection<T1 | T2>  // T1 & T2

type T = UnionToIntersection<string | number> // string & number

当传入条件类型T1 | T2时: 第一步:(U extends any ? (k: U) => void : never) 会把 union 拆分成 (T1 extends any ? (k: T1) => void : never) | (T2 extends any ? (k: T2)=> void : never),即是得到 (k: T1) => void | (k: T2) => void。

第二步:(k: T1) => void | (k: T2) => void extends ((k: infer I) => void) ? I : never,根据逆变特性,可以推断出I为T1的子集,也是T2的子集,可以推断I是T1和T2交叉类型T1&T2。

需要注意的是,3.6版本以后,空的交叉类型,会被进一步推断为never。见这里

type T1 = 'a' & 'b';  // never
type T2 = { a: string } & null;  // never in strict mode, null in non-strict mode
type T3 = { a: string } & undefined;  // never in strict mode, undefined in non-strict mode
type T4 = string & number;  // never
type T5 = number & object;  // never
type T6 = symbol & string;  // never
type T7 = void & string;  // never

另外,上面联合类型转交叉类型代码中,包含两层条件类型嵌套,其中还涉及分布式条件类型以及逆变与协变的概念。前面已经讨论过分布式条件类型,下面说一下协变和逆变。 协变和逆变讨论的是子类型的问题。具体来说比较复杂,相关概念可以看这里。这里我们只关注最终的结论。

如果A是B的子集(子类型)

  • 返回值类型是协变的,即(arg:T)=>A 是 (arg:T)=>B的子集
  • 参数类型是逆变的,即(arg:B)=>T 是 (arg:A)=>T的子集

我们用代码描述上述结论:

type Type0 = A extends B ? true : false // true
// 协变
type Type1 = ((arg: any) => A) extends ((arg: any) => B) ? true : false // true
// 逆变
type Type2 = ((arg: B) => void) extends ((arg: A) => void) ? true : false // true

因此,在infer关键字声明的待推断类型,推断其类型时,会遵循下面的原则: 如果(arg:any)=>A 是 (arg:any)=>B的子集成立,那么A是B的子集 如果(arg:A)=>void 是 (arg:B)=>void的子集成立,那么B是A的子集

至此,Typescript几个主要类型操作符都已经介绍完毕。其中穿插涉及到了一些概念,如泛型、联合类型、交叉类型、映射类型、条件类型、逆变与协变等。下面一章将从重点介绍这些高级类型开始,结合泛型和上面介绍的操作符、以及相关用例,继续讨论面向Type编程的相关操作