条件类型是在ts中是一个相对比较高级的用法,在平时开发中,大家可能直接使用得比较少,但是在ts开发中,其实无时无刻都在跟条件类型打交道,跟多内置类型工具以及第三方类型声明都少不了条件类型的使用。
什么是条件类型?
基本定义
条件类型是在Typescrip在2.8版本加入的一个新featrue,用来表达非均匀类型,即基于某个条件下表示推断给定的可能的两种类型之一。下面是条件类型的一个基本表达:
T extends U ? X : Y
官方的 定义 是这样的:
When
T
isassignabletoU
the type isX
, otherwise the type isY
.
此外,按照官方的意思,以上表达式有两个状态,要么是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();
但是,按照我们的逻辑,在我们编写的代码中,在运行时中理论上传入了字符串的话,理论上应该是返回字符串才对,但是由于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来说,是安全的,因为仅仅赋值行为,其他行为是不被允许的。
类型收窄
相对于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-…