深入讲解Ts中高级类型工具
写在最前:本文转自掘金
一、 前置内容
[key: string]
索引签名类型
索引签名类型主要指的是在接口或类型别名中,通过以下语法来快速声明一个键值类型一致的类型结构:
interface Eg1{
[key: string]: string;
}
keyof
索引查询
对应任何类型T
,keyof T
的结果为该类型上所有共有属性key的联合:
interface Eg1{ name: string; readonly age: number; } // T1的类型是 'name' | 'age' type T1 = keyof Eg1
class Eg2 {
private name: string;
public readonly age: number;
protected home: string;
}
// T2实则约束为 'age'
type T2 = keyof Eg2
T[K]
索引访问
interface Eg1{ name: string; readonly age: number; }
type V1 = Eg1['name'] // string
type V2 = Eg1['name' | 'age'] // string | number
type V2 = Eg1['name' | 'age222'] // any
type V3 = Eg1[keyof Eg1] // string | number
T[keyof T]
的方式,可以获取到T
所有key
的类型组成的联合类型;注意:如果[]
中的key有不存在T中的,则是any;因为ts也不知道该key最终是什么类型,且不会报错;
in
映射类型
而映射类型,就是使用了 PropertyKeys
联合类型的泛型,其中 PropertyKeys
多是通过 keyof 创建,然后循环遍历键名创建一个类型:
type Clone<T> = {
[K in keyof T]: T[K];
};
&
交叉类型注意点
交叉类型取的多个类型的并集,如果相同key
但类型不同,则该key
为never
interface Eg1{
name: string;
age: number;
}
interface Eg2{
color: string;
age: string;
}
type T = Eg1 & Eg2 // T的类型为{ name: string;age: never; color: string },注意,age因为两者接口内的类型不一致所有事never
// 可通过如下实例验证
const val: T = {
name: ' ',
color: ' ' ,
age: (function a(){ throw Error() })(),
}
extends 关键字特性(重点)
特性一,用于接口,表示继承
interface T1{ name: string; } interface T2{ sex: number; }
// T3 = {name: string; sex: number; age:number;}
interface T3 extends T1,T2{
age: number,
}
注意,接口支持多重继承,语法为逗号隔开。如果是type实现继承,则可以使用交叉类型 type A = B& C & D
。
特性二,表示条件类型,可用于条件判断
表示条件判断,如果前面的条件满足,则返回问号后的第一个参数,否则第二个。类似于js 的三元运算。
A extends B
,A为子类型,B为父类型 ,在接口中,属性约束宽泛为父类型,子类型应该继承父类型所有属性并加以更多属性约束。 在联合类型中,类型约束越宽泛为父类型,子类型应继承父类型基础上,缩减类型。
type A1 = 'x' extends 'x' ? 1: 2; // A1 = 1
type A2 = 'x' | 'y' extends 'x' ? 1: 2; // A2 = 2
type P<T> = T extends 'x' ? 1: 2;
type A3 = P<'x' | 'y'> // A3 = 1 | 2
为什么A2
和A3
的值不一样:
- 如果用于简单的条件判断,则是之间判断前面的类型是否可分配给后面的类型
- 若
extends
前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends
后面的类型(是一个分发的过程)。
总结,就是extends
前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断,然后最终结果组成新的联合类型。
如果再严谨一些,其实我们就得到了官方的解释:对于属于裸类型参数的检查类型,条件类型会在实例化时期自动分发到联合类型上。如果不想被分发,可以通过简单的元组类型包裹一下,就非裸类型参数了:
type P<T> = [T] extends ['x'] ? 1 : 2;
type A4 = p<'x'|'y'> // 2
我们除了可以使用数组元组包裹,还可以:
type NoDistribute<T> = T & {};
type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";
type A1 = Wrapped<number | boolean>; // "N"
type A2 = Wrapped<true | false>; // "Y"
type A3 = Wrapped<true | false | 599>; // "N"
这里有两个需要单独提出来的特殊情况,any
和 never
,any
作为参数,判断条件非any
情况下会返回判断结果的联合类型;
// 直接使用,返回联合类型 type Tmp1 = any extends string ? 1 : 2; // 1 | 2
type Tmp2<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,同样返回联合类型
type Tmp2Res = Tmp2<any>; // 1 | 2
// 如果判断条件是 any,那么仍然会进行判断
type Special1 = any extends any ? 1 : 2; // 1
type Special2<T> = T extends any ? 1 : 2;
type Special2Res = Special2<any>; // 1
而never
作为泛型参数是会返回never
// 直接使用,仍然会进行判断 type Tmp3 = never extends string ? 1 : 2; // 1
type Tmp4<T> = T extends string ? 1 : 2;
// 通过泛型参数传入,会跳过判断
type Tmp4Res = Tmp4<never>; // never
// 如果判断条件是 never,还是仅在作为泛型参数时才跳过判断
type Special3 = never extends never ? 1 : 2; // 1
type Special4<T> = T extends never ? 1 : 2;
type Special4Res = Special4<never>; // never
类型兼容性
集合论中,如果一个集合的所有元素在集合B中都存在,则A是B的子集;
类型系统中,如果一个类型的属性更具体,则该类型是子类型。(因为属性更少则说明该类型约束更宽泛,是父类型)
因此,我们得到基本结论:子类型比父类型更加具体,父类型比子类型更宽泛。下面我们也将基于类型的可赋值性、协变、逆变、双向协变等进一步讲解。
可赋值性 子类型可赋值给父类型,反之不行
interface Animal { name: string; }
interface Dog extends Animal {
break(): void;
}let a: Animal;
let b: Dog;
a = b // 子类可以赋值给更加宽泛的父类型
b = a // 反过来不行
可赋值性在联合类型中的特性
type A = 1 | 2 | 3 type B = 2 | 3 let a: A; let b: B;
a = b // 可以赋值
b = a // 不可以赋值
是不是A
的类型更多,A
就是子类型呢?恰恰相反,A
此处类型更多但表达的类型越宽泛,所有A
是父类型,B
是子类型。因此父类型不能给子类型赋值。
协变
interface Animal { name: string; } interface Dog extends Animal { break(): void; }
let Eg1: Animal;
let Eg2: Dog;
// 兼容,可以赋值
Eg1 = Eg2
let Eg3: Array<Animal>
let Eg4: Array<Dog>
// 兼容,可以赋值
Eg3 = Eg4
通过Eg3
和Eg4
来看,在Animal
和Dog
在变成数组后,Array<Dog>
依旧可以赋值给Array<Animal>
,因此对于type MakeArray = Array<any>
来说就是协变。
引用维基百科中的定义:
协变与逆变(Convariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
简单说,具有父子关系的多个类型,在通过某种构造器构造成的新的类型,如果还具有父子关系则是协变,而关系逆转了(子转父,父转子)就是逆变。
这种“型变”分为两种,一种是子类型可以赋值给父类型,叫做协变,一种是父类型可以赋值给子类型,叫做逆变。
逆变
interface Animal { name: string; }
interface Dog extends Animal {
break(): void;
}type AnimalFn = (arg:Animal) => void
type DogFn = (arg: Dog) => voidlet Eg1: AnimalFn;
let Eg2: DogFn;
Eg1 = Eg2; // 不可赋值
Eg2 = Eg1; //可赋值
理论上,Animal = Dog
是类型安全的,那么AnimalFn = DogFn
也应该类型安全猜对,为什么ts认为不安全呢?看下面例子:
let animal: AnimalFn = (arg: Animal) => {} let dog: DogFn = (arg: Dog)=>{ arg.break() }
// 假设类型安全可以赋值
animal = dog;
// 那么animal 在调用时约束的参数缺少dog所需要的参数,此时会导致错误
// animal = (arg)=>{arg.break()}
animal({name: 'cat'});
从这个例子看到,如果dog函数赋值给animal函数,那么animal函数在调用时,约束的参数是Animal,但animal实际为dog的调用,传入参数无break()
方法,此时就会出现错误。
因此,Animal
和Dog
在进行type Fn<T> = (arg: T) => void
构造器构造后,父子关系就逆转了,此时称为逆变。
双向协变
ts在函数参数的比较中实际上默认采取的策略就是双向协变:只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息(典型的就是上述的逆变)。但是实际上,这极少会发生错误,并且能够实现很多JavaScript里常见模式:
// lib.dom.d.ts 中EventListener 的接口定义 interface EventListener{ (evt: Event): void; } // 简化后的Event interface Event { readonly target: EventTarget | null; preventDefault(): void; } // 简化合并后的MouseEvent interface MouseEvent extends Event{ readonly X: number; readonly Y: number; } // 简化后的window接口 interface window{ // 简化后的addEventListener addEventListerner(type: string,listener: EventListener) }
// 日常使用
window.addEventListener('click', (e: Event)=> {})
window.addEventListener('mouseover', (e:MouseEvent) => {})
可以看到window
的listener
函数要去参数必须是Event
,但是日常使用时更多时候传入的是Event
子类型。但这里可以正常使用,正式其默认行为是双向协变的原因。可以通过tsconfig.js
中修改strictFunctionType
属性来严格控制协变和逆变。
重点,infer
关键词的功能暂时先不做详细说明,主要是用于extends
的条件类型中让ts自己推断类型,具体的可以查阅官网。但关于infer
的一些容易让人忽略的重要特性,必须提及一下:
infer
推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void; } ? U : boolean;
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // never
let Eg1: { a: (x: string | number) => void; b: (x: number | string) => void } // 父类
let Eg2: { a: (x: string) => void; b: (x: number) => void } // 子类
// 允许父类向子类赋值,为逆变,推到结果为交叉类型 never
infer
推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T3 = Foo<{ a: string; b: string }>; // string
type T4 = Foo<{ a: number; b: string }>; // string|number
let Eg3: { a: number | string; b: number | string } // 父类
let Eg4: { a: number; b: string } // 子类
// 允许子类向父类赋值,为协变,推到结果为联合类型 number| string
第二部分 ts内置类型工具原理解析
Partial
Partial<T>
将T
的所有类型变为可选的。
// 核心实现就是通过映射类型遍历T上所有的属性,
// 然后将每个属性设置为可选属性
···
type Partial<T> = {
[P in keyof T]?: T[P];
}
[P in keyof T]
通过映射类型,遍历T
上的所有属性?:
设置属性为可选的T[P]
设置类型为原来的类型
扩展一下,将制定的key
变成可选类型
/**
*主要通过K extends keyof T 约束K必须为keyof T的子类
*keyof T得到的是T的所有key组成的联合类型
*/
type PartialOptional<T, K extends keyof T>=P{
[P in K]?:T[P];
}
/**
*@example
* type Eg1 = {key1?: string; key2?: number}
*/
type Eg1 = PartialOptional<{
key1:string;
key2:number;
key3: '';
}, 'key1'|'key2'>
Readonly
/*
*主要通过映射遍历所有key,
*然后给每个key增加一个readonly修饰符
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
/*
- @example
type Eg1 = { readonly key1: string; readonly key2: number; }
*/
type Eg1 = Readonly<{
key1: string;
key2: number;
}>
Pick
挑选一组属性并组成一个新的类型。
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
/* @example
- type Eg =
*/
type Eg = Pick<{
key1:string;
key2: number;
key3: boolean;
}, 'key1'|'key3'>
Record
构造一个type
,key
为联合类型中的每个子类型,类型为T
/**
* @example
* type Eg = { a: {key1: string}; b: {key2: string} }
*/
type Eg = Record<'a' | 'b', {key1: string}>
Record具体实现
// k作为key,所有的类型仅为三种string|number|symbol ,使用keyof any表示
type Record<K extends keyof any, T> = {
[P in K]: T
}
其实,Record<string, unknown>
和Record<string, any>
是日常使用较多的形式,通常我们使用这两者来代替 object
类型。
扩展:同态与非同态。
Partial
、Readonly
和Pick
都属于同态,即其实现需要输入类型T来拷贝属性,因此属性修饰符(例如readonly ?:)都会被拷贝。可从下面栗子验证:
// type Eg = {readonly a?: string}
type Eg = Pick<{readonly a?: string}, 'a'>
从Eg
的结果来看,Pick在拷贝属性时,连带拷贝了readonly
和?:
修饰符。
- Record 是非同态的,不需要拷贝属性,因此不会拷贝属性修饰符
根据Pick
的实现,P in keyof any
并没有拷贝传入类型的属性,而其他几个工具无一例外,都是用了P in keyof T
来辅助拷贝传入类型的属性。
Exclude原理解析
Exclude<T, U>
提取存在于T
,但不存在与U
的类型组成的联合类型。
/*
* 遍历T中的所有子类型,如果该子类型约束于U(存在于U,兼容于U)
* 则返回never类型,否则返回该子类型
*/
type Exclude<T, U> = T extends U ? never: T
/*
* @example
* type Eg = 'key1'
*/
type Eg = Exclude<'key1'| 'key2', 'key2'>
注意
nerver
表示一个不存在的类型nerver
与其他类型的联合后,是没有nerver
的
// type Eg2 = string | number
type Eg2 = string | number | nerver
因此上述Eg
其实就等于key1 | never
,也就是key1
Extract
Extract<T, U>
提取联合类型T和联合类型U的所有交集。
/*
* 遍历T中的所有子类型,如果该子类型约束于U(存在于U,兼容于U)
* 则返回该子类型,否则返回never
*/
type Extract<T, U> = T extends U ? T: never
/*
* @example
* type Eg = 'key2'
*/
type Eg = Extract<'key1'| 'key2', 'key2'>
Omit原理分析
Omit<T, K>
从类型T
中剔除K
中所有属性。
type Omit<T, K> = Pick<T ,Exclude<keyof T, K>>
/*
* @example
* Eg = { key2: number; key3: boolean; }
*/
type Eg = Omit1<{key1:string;key2:number;key3:boolean},'key12'|'key1'|'key13'>
- 首先我们可以利用
Pick
提取我们需要的keys组成的类型 - 也就是
Omit = Pick<T, 我们需要的属性联合>
- 而我们所需要的属性联合,就是从T的属性联合中排除存在于联合类型K中的
- 也就是
Exclude<keyof T, K>
Parameters
Parameters 获取函数的参数类型,将每个参数类型放进一个元组中。
// 具体实现 type Parameters<T extends (...args: any) = >any> = T extends (...args: infer P) => any ? P :never;
// type Eg = [ arg1: string, arg2: number ]
type Eg = Parameters<(arg1: string, arg2: number) => void>;
Parameters
首选约束T
必须是一个函数类型,所以(..args: any) => any
替换成Function
也可以- 判断
T
是否是函数类型,如果是则使用inter P
让ts自己去推导函数的参数类型,并将推导的结果存到类型P
上,否则就返回never
infer
关键字作用是让ts自己推导类型,并将推导结果存储在其绑定的类型上。infer P
就是将结果存在类型P
上供使用infer
关键字只能在extends
条件类型上使用,不能在其他地方使用。type Eg = [ arg1: string, arg2: number ]
这是一个元组,但和我们常见的元组不同,可以理解成具名元组。实质上没有什么特殊作用,比如无法通过这个额具名去取值。个人觉得,多了语义化的表达罢了。- 定义元组的可选项,只能在最后定义
// 普通元组 type Tuple1 = [ string, number? ]; let a: Tuple1 = [ 'aa', 11 ]; let a2: Tuple1 = [ 'aa' ];
// 具名元组
type Tuple2 = [ name: string, age?: number ];
let b: Tuple2 = [ 'aa', 11 ];
let b2: Tuple2 = [ 'aa' ];
扩展:infer
实现一个推导数组所有元素的类型
type FalttenArray< T extends Arrary<any> > = T extends Arrary<infer P> ? P : never;
// Eg1 = number | string;
type Eg1 = FalttenArray<[number, string]>
// Eg2 = 1 | 'as'
type Eg2 = FalttenArray<[1 | 'as']>
ReturnType 获取函数的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
ConstructorParameters
ConstructorParameters
可以获取类的构造函数的参数类型,存在一个元组中。
/* * 核心实现还是利用infer进行推导构造函数的参数类型 */ type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
// @example type Eg = [name: string, sex?: number];
class People {
constructor(public name: string, sex?: number) {}
}
type Eg = ConstructorParameters<typeof People>
- 首先约束条件
T
为拥有构造函数的类。注意这里有个abstract
修饰符,等下说明。 - 判断
T
是满足约束的类时,利用infer P
自动推导出构造函数的参数类型,并最终返回该类型。 - 其中
new (...args: any)
为构造签名,new (...args: any) => any
为构造函数类型字面量
那么,为什么要对T
约束为abstract
抽象类呢?看下面栗子:
class MyClass {} // 定义一个普通类
abstract class MyAbstractClass {} // 定义一个抽象类
let c1: typeof MyClass = MyClass // 可以赋值
let c2: typeof MyClass = MyAbstractClass // 报错,无法将抽象构造函数类型分配给非抽象构造函数类型
let c3: typeof MyAbstractClass = MyClass //可以赋值
let c4: typeof MyAbstractClass = MyAbstractClass //可以赋值
由此可以看出,可以将抽象类(抽象构造函数)赋值给抽象类或者普通类,反之不行。
那么,为什么使用typeof 类
作为类型呢,直接使用类作为类型又有什么区别呢?
// 定义一个类 class People{ name: string; age: number; constructor() {} } let p1: People = new People // 可以赋值 let p2: People = People // 不可以赋值 等号后面缺少name, age
let p3: typeof People = People // 可以赋值
let p4: typeof People = new People() // 不可以赋值,p4缺少prototype
简单的理解就是
typeof 类
作为类型,需要赋值为类本身类
作为类型,需要赋值为类的实例
最后,只需要对infer
的使用换个位置,便可以获取构造函数返回值的类型:
type InstanceType<T extends abstract new (...args: any)=> any> = T extends abstract new (...args: any) => infer R ? R :any;
作者:没名字的某某人
链接:https://www.jianshu.com/p/c1e5384d6255
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。