TS中的条件类型

2,234 阅读8分钟

条件类型是在ts中是一个相对比较高级的用法,在平时开发中,大家可能直接使用得比较少,但是在ts开发中,其实无时无刻都在跟条件类型打交道,跟多内置类型工具以及第三方类型声明都少不了条件类型的使用。

什么是条件类型?

基本定义

条件类型是在Typescrip在2.8版本加入的一个新featrue,用来表达非均匀类型,即基于某个条件下表示推断给定的可能的两种类型之一。下面是条件类型的一个基本表达:

T extends U ? X : Y

官方的 定义 是这样的:

When T isassignableto U the type is X , otherwise the type is Y .

此外,按照官方的意思,以上表达式有两个状态,要么是resolved要么是deffered,官方定义比较晦涩,这里可以先看例子:

type StringOnly<T> = T extends string ? never : T;
type A = StringOnly<string >; // string
type B = StringOnly<number >; // never

如果T extends U,意味着T is assignable to U,T分配给U是安全的,有时候ts类型之间的assignable可能会有些难理解,有些关于assignable会继续说到。

分配式条件类型

根据官方定义,在T extends U ? X : Y中, 当类型参数T是A|B|C时,以下两个表达式等价

A|B|C extends U ? X : Y
A extends U ? X : Y| B extends U ? X : Y| C extends U ? X : Y

这个定律有时候可以帮助我们快速分析类型推断,看下面一个例子:

type Diff<T, U> = T extends U ? T : never;  // Remove types from T that are assignable to U
type T1= Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; 

type T2= Diff<"a", "a" | "c" | "f"> | Diff<"b", "a" | "c" | "f"> | Diff<"c", "a" | "c" | "f"> 

type T3= "a"|"c"

// T1===T2===T3

infer推断

在T extends U ? X : Y中,类型U可以使用infer关键词来指定一个新的推断类型,这表示,如果T 可分配给U,那么推断这个新的类型。

type FirstParam<T> = T extends (a: infer R) => void ? R : never;
type param = FirstParam<(a: number) => void>; // numbder

有了这个infer关键词,我们可以很容易实现,从一些高级类型中”取出“一些值。

type PromiseValue<T extends Promise<any>> = T extends Promise<infer R> ? R : any; //取出Promise的值,
PromiseValue<Promise<number>> // number

type ArrayValue<T extends any[]> = T extends (infer R)[] ? R : any;
ArrayValue<number[]> // number

type ValueOf<T extends {}> = T extends { [key: string]: infer R } ? R : any;
ValueOf<{a:number,b:string}>// number|string

infer可以多次使用,推断一个或多个类型的组合

type Foo<T> = T extends { a: infer U, b: infer R} ? [U,R] : never;
type Foo<T> = T extends { a: infer U, b: infer U} ? U : never;

另外,可以在U中,可以在多个位置推断一个类型,这种情况下,推断的结果可能是一个联合类型或者交叉类型,这取决于推断类型所处在的位置。

如果推断的类型是在协变位置(covariant)时,那么推断结果是一个联合类型。如:

type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>;  // string
type T11 = Foo<{ a: string, b: number }>;  // string | number

如果推断的类型是在逆变位置(contravariance)时,那么推断结果是一个交叉类型。如:

type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never;
type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>;  // string
type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>;  // string & number

协变逆变是用来描述父子类型之间的关系的,这里不再展开,有兴趣可以自行搜索相关资料,这里有一个ts的协变与逆变的解析,也可以看看( 传送门

内置类型

Ts中内置了一些比较常用的工具函数,它们都是基于条件类型实现的,主要有以下:

Exclude

定义

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

例子

type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T02 = Exclude<string | number | (() => void), Function>;  // string | number

Extract

定义

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

例子

type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"
type T03 = Extract<string | number | (() => void), Function>;  // () => void

NonNullable

定义

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends void ? never : T;

例子

type T11 = NonNullable<string | undefined>; // string

Parameters

定义

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

例子

type T12 = Parameters<(s: string) => void>;  // [string]

ReturnType

定义

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

例子

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void

其他应用场景

函数返回值推断更加方便

我们在实际写代码的时候,往往是有较多分支的,不同分支,可能对应着不同的返回类型,比如以下例子:

function process(text:string|undefined){
    return text && text.replace(/f/g, 'p') ;
}

如果我们在业务代码中这样写的话,在ts中是编译不过的

process('asda').toLocaleLowerCase();

image-1-1619086728181.png

但是,按照我们的逻辑,在我们编写的代码中,在运行时中理论上传入了字符串的话,理论上应该是返回字符串才对,但是由于ts编译器由于只能进行静态分析,只会按照返回值的类型自动推导。

这个问题怎么解决呢? 在没有条件类型前,对于函数返回值问题,我们通常的做法是使用ts的函数声明重载功能,

function process(text:string):string 
function process(text:undefined):undefined 
function process(text:any):any 

function process(text: string | undefined) {
    return text && text.replace(/f/g, 'p');
}

process('asda').toLocaleLowerCase(); // it work

有了条件类型后,可以更轻松实现,瞬间干净了。

function process<T extends string | null>(text: T): T extends string ? string : null {
    return text && (text.replace(/f/g, 'p') as any);
}

可分配性

结构化类型

上面提到过,T extends U 表示类型T为变量可安全地赋值给类型为U的变量,下面看个例子:

declare const a: string
const b: string = a

a是安全地赋值给b的,因为 string 可分配给string。

在ts中决定类型之间的可分配性是基于结构化类型(structural typing)的,什么是结构化类型?这个结构化的类型有什么表现呢?可能大家都听过一个叫做"鸭子类型"的谚语:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

比如:

interface Cat{
    name:string
    id:number
}
interface People{
    name:string
}
declare const cat:Cat

const people:People=cat // work

在ts的类型系统中,如果T可分配给U,ts只关心这个类型的表现是什么,不关心这个类型叫什么名称,只要T的表现跟U一样,ts认为T可分配给U是安全的。

虽然这种类型系统十分便捷,但是有时候跟js表现是不一致的,比如:

class A {}
class B {}

const b: B = new A() // ✔ all good
const a: A = new B() // ✔ all good

new A() instanceof B // => false

另一种类型系统叫做"名义类型"(nominal typings),在ts中是可以有些方法可以做到像名义类型那样,这里不展开,有兴趣的同学可以看看这里( 传送门

字面量类型

ts中,字面量也是一种类型,字面量类型一般是ts基础类型的的一个特例。某个类型的特例是某个类型的自类型,如:

declare const apple: 'apple';

const another_apple: 'apple' = apple;

const banana: 'banana' = apple; // error

const another_banana: string = apple; // it work

TopType

类型理论中有个叫顶层类型的概念,所有类型都可以分配给顶级类型。在ts中,顶层类型又any以及unknown两个顶级类型。

Any types

let value: any;  
value = true;             // OK
value = 42;               // OK 
value = "Hello World";    // OK 
value = [];               // OK 
value = {};               // OK 
value = Math.random;      // OK 
value = null;             // OK 
value = undefined;        // OK 
value = new TypeError();  // OK 
value = Symbol("type");   // OK

我们通常说any大法好,好就好在any可以允许我们脱离ts的约束,重新回到js的编程体验中,比如:

let value: any;
value = 1234;
value.toString();
value.a.b.c;

通常上面这样,运行时都是不安全的,所以,在ts3.0中加入了另外一个顶级类型unkown。

Unknown types

作为顶级类型,所有其他类型都可以直接赋值,这里跟any一样。

let value: unknown;  
value = true;             // OK
value = 42;               // OK 
value = "Hello World";    // OK 
value = [];               // OK 
value = {};               // OK 
value = Math.random;      // OK 
value = null;             // OK 
value = undefined;        // OK 
value = new TypeError();  // OK 
value = Symbol("type");   // OK

但是unkown相对any来说,是安全的,因为仅仅赋值行为,其他行为是不被允许的。 image-2-1619086730609.png

类型收窄

相对于any,我们通常是需要类型收窄来断言类型,以获得相应类型的操作方法,所以unknown是安全的。

比如:

function split(params: unknown) {
    if (typeof params === 'string') {
        return params.split('');
    } else if (params instanceof Number) {
        return params.toString().split('');
    }
    throw 'error';
}

除了typeof 、instanceof 外,还可以通过自定义保护函数来保证类型的正确性:

function isNumber(input: unknown): input is number {
    return typeof input === 'number';
}
function square(params: unknown) {
    if (isNumber(params)) {
        return params * params;
    }
    throw 'error';
}

ts官方也用了大量的自定义保护函数,适合平时在写一些工具函数判断的时候,可以使用这种方法来收窄类型。

交叉与联合

顶层类型与其他类型联合后,依旧是顶层类型。any与unknown 联合为any

type T100 = 'asd' | any | number | null; // any
type T101 = 'asd' | unknown | number | null; // unknown
type T102 = 'asd' | unknown | any | null; // any

any与所有其他类型交叉都是any;

type T200 = string & any ; // any
type T202 = any | unknown ; // any

unknown与其他类型交叉后是其他类型

type T201 = string & unknown ;// string

BottomType

有顶层类型,自然有底层类型,在ts中,底层类型是never,表示其他任意类型的值都不能赋值给该值,包括any、unknown。

let value1: never;
let value2: any;
let value3: unknown;

value1 = ''; // error
value1 = 2; // error
value1 = ()=>{}; // error
value1 = []; // error
value1=value2 // error
value1=value3 // error

除了显式注解一个变量的类型为never外,以下情况也会被推断为never,表示运行时永远不会往下跑。

  • 抛出异常
function fail(): never {
    throw 'error';
}
let value1: never;
value1 = fail();
  • 死循环
function loop(): never {
    while(true){

    }
}

let value1: never;
value1 = loop();

交叉与联合

never与其他类型交叉后,都是其他类型;

type T301 = string | never; // string
type T302 = 'string' | never; // 'string'

交叉后是never;

type T401 = string & never; // never
type T402 = 'string' & never; // never
type T403 = any & never; // never

参考

artsy.github.io/blog/2018/1… github.com/microsoft/T… zhuanlan.zhihu.com/p/60253127 medium.com/better-prog… medium.com/better-prog… thesoftwaresimpleton.com/blog/2019/0… jkchao.github.io/typescript-…