Ts高手篇:22个示例深入讲解Ts最晦涩难懂的高级类型工具
Hello大家好,我是愣锤。随着Typescript不可阻挡的趋势,相信小伙伴们或多或少的使用过Ts开发了。而Ts的使用除了基本的类型定义外,对于Ts的泛型、内置高级类型、自定义高级类型工具等会相对陌生。本文将会通过22个类型工具例子,深入讲解Ts类型工具原理和编程技巧。不扯闲篇,全程干货,内容非常多,想提升Ts功力的小伙伴请耐心读下去。相信小伙伴们在读完此文后,能够对这块有更深入的理解。下面,我们开始吧~
本文基本分为三部分:
第一部分讲解一些基本的关键词的特性(比如索引查询、索引访问、映射、extends等),但是该部分更多的讲解小伙伴们不清晰的一些特性,而基本功能则不再赘述。更多的关键词及技巧将包含在后续的例子演示中再具体讲述;第二部分讲解Ts内置的类型工具以及实现原理,比如Pick、Omit等;第三部分讲解自定义的工具类型,该部分也是最难的部分,将通过一些复杂的类型工具示例进行逐步剖析,对于其中的晦涩的地方以及涉及的知识点逐步讲解。此部分也会包含大量Ts类型工具的编程技巧,也希望通过此部分的讲解,小伙伴的Ts功底可以进一步提升!第一部分 前置内容keyof 索引查询对应任何类型T,keyof T的结果为该类型上所有共有属性key的联合:
interfaceEg1{
name:string,
readonlyage:number,
}
//T1的类型实则是name|age
typeT1=keyofEg1
classEg2{
privatename:string;
publicreadonlyage:number;
protectedhome:string;
}
//T2实则被约束为age
//而name和home不是公有属性,所以不能被keyof获取到
typeT2=keyofEg2
T[K] 索引访问interfaceEg1{
name:string,
readonlyage:number,
}
//string
typeV1=Eg1['name']
//string|number
typeV2=Eg1['name'|'age']
//any
typeV2=Eg1['name'|'age2222']
//string|number
typeV3=Eg1[keyofEg1]
T[keyof T]的方式,可以获取到T所有key的类型组成的联合类型;T[keyof K]的方式,获取到的是T中的key且同时存在于K时的类型组成的联合类型;注意:如果[]中的key有不存在T中的,则是any;因为ts也不知道该key最终是什么类型,所以是any;且也会报错;
& 交叉类型注意点交叉类型取的多个类型的并集,但是如果相同key但是类型不同,则该key为never。
interfaceEg1{
name:string,
age:number,
}
interfaceEg2{
color:string,
age:string,
}
/**
*T的类型为{name:string;age:number;age:never}
*注意,age因为Eg1和Eg2中的类型不一致,所以交叉后age的类型是never
*/
typeT=Eg1Eg2
//可通过如下示例验证
constval:T={
name:'',
color:'',
age:(functiona(){
throwError()
})(),
}
extends关键词特性(重点)用于接口,表示继承interfaceT1{
name:string,
}
interfaceT2{
sex:number,
}
/**
*@example
*T3={name:string,sex:number,age:number}
*/
interfaceT3extendsT1,T2{
age:number,
}
注意,接口支持多重继承,语法为逗号隔开。如果是type实现继承,则可以使用交叉类型type A = B & C & D。
表示条件类型,可用于条件判断表示条件判断,如果前面的条件满足,则返回问号后的第一个参数,否则第二个。类似于js的三元运算。
/**
*@example
*typeA1=1
*/
typeA1='x'extends'x'?1:2;
/**
*@example
*typeA2=2
*/
typeA2='x'|'y'extends'x'?1:2;
/**
*@example
*typeA3=1|2
*/
typeP=Textends'x'?1:2;
typeA3=P'x'|'y'
提问:为什么A2和A3的值不一样?
如果用于简单的条件判断,则是直接判断前面的类型是否可分配给后面的类型若extends前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends后面的类型(是一个分发的过程)。总结,就是extends前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断。然后将最终的结果组成新的联合类型。
阻止extends关键词对于联合类型的分发特性如果不想被分解(分发),做法也很简单,可以通过简单的元组类型包裹以下:
typeP=[T]extends['x']?1:2;
/**
*typeA4=
*/
typeA4=P'x'|'y'
条件类型的分布式特性文档
类型兼容性“集合论中,如果一个集合的所有元素在集合B中都存在,则A是B的子集;
类型系统中,如果一个类型的属性更具体,则该类型是子类型。(因为属性更少则说明该类型约束的更宽泛,是父类型)
”因此,我们可以得出基本的结论:子类型比父类型更加具体,父类型比子类型更宽泛。 下面我们也将基于类型的可复制性(可分配性)、协变、逆变、双向协变等进行进一步的讲解。
可赋值性interfaceAnimal{
name:string;
}
interfaceDogextendsAnimal{
break():void;
}
leta:Animal;
letb:
//可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型
a=
//反过来不行
b=
可赋值性在联合类型中的特性typeA=1|2|3;
typeB=2|3;
leta:
letb:
//不可赋值
b=
//可以赋值
a=
是不是A的类型更多,A就是子类型呢?恰恰相反,A此处类型更多但是其表达的类型更宽泛,所以A是父类型,B是子类型。
因此b = a不成立(父类型不能赋值给子类型),而a = b成立(子类型可以赋值给父类型)。
协变interfaceAnimal{
name:string;
}
interfaceDogextendsAnimal{
break():void;
}
letEg1:Animal;
letEg2:
//兼容,可以赋值
Eg1=
letEg3:ArrayAnimal
letEg4:ArrayDog
//兼容,可以赋值
Eg3=Eg4
通过Eg3和Eg4来看,在Animal和Dog在变成数组后,ArrayDog依旧可以赋值给ArrayAnimal,因此对于type MakeArray = Arrayany来说就是协变的。
最后引用维基百科中的定义:
“协变与逆变(Covariance and contravariance )是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
”简单说就是,具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变的,而关系逆转了(子变父,父变子)就是逆变的。可能听起来有些抽象,下面我们将用更具体的例子进行演示说明:
逆变interfaceAnimal{
name:string;
}
interfaceDogextendsAnimal{
break():void;
}
typeAnimalFn=(arg:Animal)=void
typeDogFn=(arg:Dog)=void
letEg1:AnimalFn;
letEg2:DogFn;
//不再可以赋值了,
//AnimalFn=DogFn不可以赋值了,Animal=Dog是可以的
Eg1=
//反过来可以
Eg2=
理论上,Animal = Dog是类型安全的,那么AnimalFn = DogFn也应该类型安全才对,为什么Ts认为不安全呢?看下面的例子:
letanimal:AnimalFn=(arg:Animal)={}
letdog:DogFn=(arg:Dog)={
arg.break();
}
//假设类型安全可以赋值
animal=
//那么animal在调用时约束的参数,缺少dog所需的参数,此时会导致错误
animal({name:'cat'});
从这个例子看到,如果dog函数赋值给animal函数,那么animal函数在调用时,约束的是参数必须要为Animal类型(而不是Dog),但是animal实际为dog的调用,此时就会出现错误。
因此,Animal和Dog在进行type Fn = (arg: T) = void构造器构造后,父子关系逆转了,此时成为“逆变”。
双向协变Ts在函数参数的比较中实际上默认采取的策略是双向协变:只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。
这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息(典型的就是上述的逆变)。但是实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式:
//lib.dom.d.ts中EventListener的接口定义
interfaceEventListener{
(evt:Event):void;
}
//简化后的Event
interfaceEvent{
readonlytarget:EventTarget|null;
preventDefault():void;
}
//简化合并后的MouseEvent
interfaceMouseEventextendsEvent{
readonlyx:number;
readonlyy:number;
}
//简化后的Window接口
interfaceWindow{
//简化后的addEventListener
addEventListener(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推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型。typeBar=Textends{
a:(x:inferU)=void;
b:(x:inferU)=void;
}?U:never;
//typeT1=string
typeT1=Bar{a:(x:string)=void;b:(x:string)=void}
//typeT2=never
typeT2=Bar{a:(x:string)=void;b:(x:number)=void}
infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型。typeFoo=Textends{
a:infer
b:infer
}?U:never;
//typeT1=string
typeT1=Foo{a:string;b:string}
//typeT2=string|number
typeT2=Foo{a:string;b:number}
inter与协变逆变的参考文档点击这里
企业微信截图_8357a6f0-aa88-4faf-b21e-f1baa6bc790e.png第二部分 Ts内置类型工具原理解析Partial实现原理解析Partial将T的所有属性变成可选的。
/**
*核心实现就是通过映射类型遍历T上所有的属性,
*然后将每个属性设置为可选属性
*/
typePartial={
[PinkeyofT]?:T[P];
}
[P in keyof T]通过映射类型,遍历T上的所有属性?:设置为属性为可选的T[P]设置类型为原来的类型扩展一下,将制定的key变成可选类型:
/**
*主要通过KextendskeyofT约束K必须为keyofT的子类型
*keyofT得到的是T的所有key组成的联合类型
*/
typePartialOptionalT,KextendskeyofT={
[PinK]?:T[P];
}
/**
*@example
*typeEg1={key1?:string;key2?:number}
*/
typeEg1=PartialOptional{
key1:string,
key2:number,
key3:''
},'key1'|'key2'
Readonly原理解析/**
*主要实现是通过映射遍历所有key,
*然后给每个key增加一个readonly修饰符
*/
typeReadonly={
readonly[PinkeyofT]:T[P]
}
/**
*@example
*typeEg={
*readonlykey1:string;
*readonlykey2:number;
*}
*/
typeEg=Readonly{
key1:string,
key2:number,
}
Pick挑选一组属性并组成一个新的类型。
typePickT,KextendskeyofT={
[PinK]:T[P];
};
基本和上述同样的知识点,就不再赘述了。
Record构造一个type,key为联合类型中的每个子类型,类型为T。文字不好理解,先看例子:
/**
*@example
*typeEg1={
*a:{key1:string;
*b:{key1:string;
*}
*@desc就是遍历第一个参数'a'|'b'的每个子类型,然后将值设置为第二参数
*/
typeEg1=Record'a'|'b',{key1:string}
Record具体实现:
/**
*核心实现就是遍历K,将值设置为T
*/
typeRecordKextendskeyofany,T={
[PinK]:T
}
/**
*@example
*typeEg2={a:B,b:B}
*/
interfaceA{
a:string,
b:number,
}
interfaceB{
key1:number,
key2:string,
}
typeEg2=RecordkeyofA,B
值得注意的是keyof any得到的是string | number | symbol原因在于类型key的类型只能为string | number | symbol扩展: 同态与非同态。划重点!!!划重点!!!划重点!!!
Partial、Readonly和Pick都属于同态的,即其实现需要输入类型T来拷贝属性,因此属性修饰符(例如readonly、?:)都会被拷贝。可从下面例子验证:/**
*@example
*typeEg={readonlya?:string}
*/
typeEg=Pick{readonlya?:string},'a'
从Eg的结果可以看到,Pick在拷贝属性时,连带拷贝了readonly和?:的修饰符。
Record是非同态的,不需要拷贝属性,因此不会拷贝属性修饰符可能到这里就有小伙伴疑惑了,为什么Pick拷贝了属性,而Record没有拷贝?我们来对比一下其实现:
typePickT,KextendskeyofT={
[PinK]:T[P];
};
typeRecordKextendskeyofany,T={
[PinK]:T
}
可以看到Pick的实现中,注意P in K(本质是P in keyof T),T为输入的类型,而keyof T则遍历了输入类型;而Record的实现中,并没有遍历所有输入的类型,K只是约束为keyof any的子类型即可。
最后再类比一下Pick、Partial、readonly这几个类型工具,无一例外,都是使用到了keyof T来辅助拷贝传入类型的属性。
Exclude原理解析ExcludeT, U提取存在于T,但不存在于U的类型组成的联合类型。
/**
*遍历T中的所有子类型,如果该子类型约束于U(存在于U、兼容于U),
*则返回never类型,否则返回该子类型
*/
typeExcludeT,U=TextendsU?never:
/**
*@example
*typeEg='key1'
*/
typeEg=Exclude'key1'|'key2','key2'
敲重点!!!
never表示一个不存在的类型never与其他类型的联合后,是没有never的/**
*@example
*typeEg2=string|number
*/
typeEg2=string|number|never
因此上述Eg其实就等于key1 | never,也就是type Eg = key1
ExtractExtractT, U提取联合类型T和联合类型U的所有交集。
typeExtractT,U=TextendsU?T:never;
/**
*@example
*typeEg='key1'
*/
typeEg=Extract'key1'|'key2','key1'
Omit原理解析OmitT, K从类型T中剔除K中的所有属性。
/**
*利用Pick实现Omit
*/
typeOmit=PickT,ExcludekeyofT,K
换种思路想一下,其实现可以是利用Pick提取我们需要的keys组成的类型因此也就是 Omit = PickT, 我们需要的属性联合而我们需要的属性联合就是,从T的属性联合中排出存在于联合类型K中的因此也就是Excludekeyof T, K;如果不利用Pick实现呢?
/**
*利用映射类型Omit
*/
typeOmit2T,Kextendskeyofany={
[PinExcludekeyofT,K]:T[P]
}
其实现类似于Pick的原理实现区别在于是遍历的我们需要的属性不一样我们需要的属性和上面的例子一样,就是Excludekeyof T, K因此,遍历就是[P in Excludekeyof T, K]Parameters 和 ReturnTypeParameters 获取函数的参数类型,将每个参数类型放在一个元组中。
/**
*@desc具体实现
*/
typeParametersTextends(...args:any)=any=Textends(...args:inferP)=any?P:never;
/**
*@example
*typeEg=[arg1:string,arg2:number];
*/
typeEg=Parameters(arg1:string,arg2:number)=void
Parameters首先约束参数T必须是个函数类型,所以(...args: any) = any替换成Function也是可以的具体实现就是,判断T是否是函数类型,如果是则使用inter P让ts自己推导出函数的参数类型,并将推导的结果存到类型P上,否则就返回never;敲重点!!!敲重点!!!敲重点!!!
infer关键词作用是让Ts自己推导类型,并将推导结果存储在其参数绑定的类型上。Eg:infer P 就是将结果存在类型P上,供使用。infer关键词只能在extends条件类型上使用,不能在其他地方使用。再敲重点!!!再敲重点!!!再敲重点!!!
type Eg = [arg1: string, arg2: number]这是一个元组,但是和我们常见的元组type tuple = [string, number]。官网未提到该部分文档说明,其实可以把这个作为类似命名元组,或者具名元组的意思去理解。实质上没有什么特殊的作用,比如无法通过这个具名去取值不行的。但是从语义化的角度,个人觉得多了语义化的表达罢了。
定义元祖的可选项,只能是最后的选项
/**
*普通方式
*/
typeTuple1=[string,number?];
consta:Tuple1=['aa',11];
consta2:Tuple1=['aa'];
/**
*具名方式
*/
typeTuple2=[name:string,age?:number];
constb:Tuple2=['aa',11];
constb2:Tuple2=['aa'];
扩展:infer实现一个推导数组所有元素的类型:
/**
*约束参数T为数组类型,
*判断T是否为数组,如果是数组类型则推导数组元素的类型
*/
typeFalttenArrayTextendsArrayany=TextendsArrayinferP?P:never;
/**
*typeEg1=number|string;
*/
typeEg1=FalttenArray[number,string]
/**
*typeEg2=1|'asd';
*/
typeEg2=FalttenArray[1,'asd']
ReturnType 获取函数的返回值类型。
/**
*@descReturnType的实现其实和Parameters的基本一样
*无非是使用infer R的位置不一样。
*/
typeReturnTypeTextends(...args:any)=any=Textends(...args:any)=inferR?R:any;
ConstructorParametersConstructorParameters可以获取类的构造函数的参数类型,存在一个元组中。
/**
*核心实现还是利用infer进行推导构造函数的参数类型
*/
typeConstructorParametersTextendsabstractnew(...args:any)=any=Textendsabstractnew(...args:inferP)=any?P:never;
/**
*@example
*typeEg=string;
*/
interfaceErrorConstructor{
new(message?:string):Error;
(message?:string):Error;
readonlyprototype:Error;
}
typeEg=ConstructorParametersErrorConstructor
/**
*@example
*typeEg2=[name:string,sex?:number];
*/
classPeople{
constructor(publicname:string,sex?:number){}
}
typeEg2=ConstructorParameterstypeofPeople
首先约束参数T为拥有构造函数的类。注意这里有个abstract修饰符,等下会说明。实现时,判断T是满足约束的类时,利用infer P自动推导构造函数的参数类型,并最终返回该类型。敲重点!!!敲重点!!!敲重点!!!
那么疑问来了,为什么要对T要约束为abstract抽象类呢?看下面例子:
/**
*定义一个普通类
*/
classMyClass{}
/**
*定义一个抽象类
*/
abstractclassMyAbstractClass{}
//可以赋值
constc1:typeofMyClass=MyClass
//报错,无法将抽象构造函数类型分配给非抽象构造函数类型
constc2:typeofMyClass=MyAbstractClass
//可以赋值
constc3:typeofMyAbstractClass=MyClass
//可以赋值
constc4:typeofMyAbstractClass=MyAbstractClass
由此看出,如果将类型定义为抽象类(抽象构造函数),则既可以赋值为抽象类,也可以赋值为普通类;而反之则不行。
再敲重点!!!再敲重点!!!再敲重点!!!
这里继续提问,直接使用类作为类型,和使用typeof 类作为类型,有什么区别呢?
/**
*定义一个类
*/
classPeople{
name:number;
age:number;
constructor(){}
}
//p1可以正常赋值
constp1:People=newPeople();
//等号后面的People报错,类型“typeofPeople”缺少类型“People”中的以下属性:name,age
constp2:People=People;
//p3报错,类型"People"中缺少属性"prototype",但类型"typeofPeople"中需要该属性
constp3:typeofPeople=newPeople();
//p4可以正常赋值
constp4:typeofPeople=People;
结论是这样的:
当把类直接作为类型时,该类型约束的是该类型必须是类的实例;即该类型获取的是该类上的实例属性和实例方法(也叫原型方法);当把typeof 类作为类型时,约束的满足该类的类型;即该类型获取的是该类上的静态属性和方法。
最后,只需要对infer的使用换个位置,便可以获取构造函数返回值的类型:
typeInstanceTypeTextendsabstractnew(...args:any)=any=Textendsabstractnew(...args:any)=inferR?R:any;
Ts compiler内部实现的类型Uppercase/**
*@desc构造一个将字符串转大写的类型
*@example
*typeEg1='ABCD';
*/
typeEg1=Uppercase'abcd'
Lowercase/**
*@desc构造一个将字符串转小大写的类型
*@example
*typeEg2='abcd';
*/
typeEg2=Lowercase'ABCD'
Capitalize/**
*@desc构造一个将字符串首字符转大写的类型
*@example
*typeEg3='abcd';
*/
typeEg3=Capitalize'Abcd'
Uncapitalize/**
*@desc构造一个将字符串首字符转小写的类型
*@example
*typeEg3='ABCD';
*/
typeEg3=Uncapitalize'aBCD'
这些类型工具,在lib.es5.d.ts文件中是看不到具体定义的:
typeUppercaseSextendsstring=intrinsic;
typeLowercaseSextendsstring=intrinsic;
typeCapitalizeSextendsstring=intrinsic;
typeUncapitalizeSextendsstring=intrinsic;
企业微信截图_1900dfc9-3c22-4af2-9523-6860bcf03e03.png第三部分 自定义Ts高级类型工具及类型编程技巧SymmetricDifferenceSymmetricDifferenceT, U获取没有同时存在于T和U内的类型。
/**
*核心实现
*/
typeSymmetricDifferenceA,B=SetDifferenceA|B,AB
/**
*SetDifference的实现和Exclude一样
*/
typeSymmetricDifferenceT,U=ExcludeT|U,TU
/**
*@example
*typeEg='1'|
*/
typeEg=SymmetricDifference'1'|'2'|'3','2'|'3'|'4'
其核心实现利用了3点:分发式联合类型、交叉类型和Exclude。
首先利用Exclude从获取存在于第一个参数但是不存在于第二个参数的类型Exclude第2个参数是T & U获取的是所有类型的交叉类型Exclude第一个参数则是T | U,这是利用在联合类型在extends中的分发特性,可以理解为ExcludeT, T | ExcludeU, T ;总结一下就是,提取存在于T但不存在于T & U的类型,然后再提取存在于U但不存在于T & U的,最后进行联合。
FunctionKeys获取T中所有类型为函数的key组成的联合类型。
/**
*@descNonUndefined判断T是否为undefined
*/
typeNonUndefined=Textendsundefined?never:
/**
*@desc核心实现
*/
typeFunctionKeysTextendsobject={
[KinkeyofT]:NonUndefinedT[K]extendsFunction?K:never;
}[keyof
/**
*@example
*typeEg='key2'|'key3';
*/
typeAType={
key1:string,
key2:()=void,
key3:Function,
};
typeEg=FunctionKeysAType
首先约束参数T类型为object通过映射类型K in keyof T遍历所有的key,先通过NonUndefinedT[K]过滤T[K]为undefined | null的类型,不符合的返回never若T[K]为有效类型,则判断是否为Function类型,是的话返回K,否则never;此时可以得到的类型,例如:/**
*上述的Eg在此时应该是如下类型,伪代码:
*/
typeTempType={
key1:never,
key2:'key2',
key3:'key3',
}
最后经过{省略}[keyof T]索引访问,取到的为值类型的联合类型never | key2 | key3,计算后就是key2 | key3;敲重点!!!敲重点!!!敲重点!!!
T[]是索引访问操作,可以取到值的类型T['a' | 'b']若[]内参数是联合类型,则也是分发索引的特性,依次取到值的类型进行联合T[keyof T]则是获取T所有值的类型类型;never和其他类型进行联合时,never是不存在的。例如:never | number | string等同于number | string再敲重点!!!再敲重点!!!再敲重点!!!
null和undefined可以赋值给其他类型(开始该类型的严格赋值检测除外),所以上述实现中需要使用NonUndefined先行判断。NonUndefined中的实现,只判断了T extends undefined,其实也是因为两者可以互相兼容的。所以你换成T extends null或者T extends null | undefined都是可以的。//A=1
typeA=undefinedextendsnull?1:2;
//B=1
typeB=nullextendsundefined?1:2;
最后,如果你想写一个获取非函数类型的key组成的联合类型,无非就是K和never的位置不一样罢了。同样,你也可以实现StringKeys、NumberKeys等等。但是记得可以抽象个工厂类型哈:
typePrimitive=
|string
|number
|bigint
|boolean
|symbol
|null
|undefined;
/**
*@desc用于创建获取指定类型工具的类型工厂
*@paramT待提取的类型
*@paramP要创建的类型
*@paramIsCheckNon是否要进行null和undefined检查
*/
typeKeysFactoryT,PextendsPrimitive|Function|object,IsCheckNonextendsboolean={
[KinkeyofT]:IsCheckNonextendstrue
?(NonUndefinedT[K]extendsP?K:never)
:(T[K]extendsP?K:never);
}[keyof
/**
*@example
*例如上述KeysFactory就可以通过工厂类型进行创建了
*/
typeFunctionKeys=KeysFactoryT,Function,true
typeStringKeys=KeysFactoryT,string,true
typeNumberKeys=KeysFactoryT,string,true
MutableKeysMutableKeys查找T所有可选类型的key组成的联合类型。
/**
*核心实现
*/
typeMutableKeysTextendsobject={
[PinkeyofT]-?:IfEquals
{[QinP]:T[P]},
{-readonly[QinP]:T[P]},
P
;
}[keyof
/**
*@desc一个辅助类型,判断X和Y是否类型相同,
*@returns是则返回A,否则返回B
*/
typeIfEqualsX,Y,A=X,B=never=(()=TextendsX?1:2)extends(()=TextendsY?1:2)
?A
:B;
MutableKeys还是有一定难度的,讲解MutableKeys的实现,我们要分下面几个步骤:
第一步,先理解只读和非只读的一些特性
/**
*遍历类型T,原封不动的返回,有点类似于拷贝类型的意思
*/
typeRType1={
[PinkeyofT]:T[P];
}
/**
*遍历类型T,将每个key变成非只读
*或者理解成去掉只读属性更好理解。
*/
typeRType2={
-readonly[PinkeyofT]:T[P];
}
//R0={a:string;readonlyb:number}
typeR0=RType1{a:string,readonlyb:number}
//R1={a:string}
typeR1=RType1{a:string}
//R2={a:string}
typeR2=RType2{a:string}
//R3={readonlya:string}
typeR3=RType1{readonlya:string}
//R4={a:string}
typeR4=RType2{readonlya:string}
可以看到:RType1和RType2的参数为非只读的属性时,R1和R2的结果是一样的;RType1和RType2的参数为只读的属性时,得到的结果R3是只读的,R4是非只读的。所以,这里要敲个重点了:
[P in Keyof T]是映射类型,而映射是同态的,同态即会拷贝原有的属性修饰符等。可以参考R0的例子。映射类型上的-readonly表示为非只读,或者可以理解为去掉只读。对于只读属性加上-readonly变成了非只读,而对非只读属性加上-readonly后还是非只读。一种常见的使用方式,比如你想把属性变成都是非只读的,不能前面不加修饰符(虽然不写就表示非只读),但是要考虑到同态拷贝的问题。第二步,解析IfEquals
IfEquals用于判断类型X和Y是否相同,相等则返回A,否则返回B。这个函数是比较难的,也别怕啦,下面讲完就妥妥的明白啦~
typeIfEqualsX,Y,A=X,B=never=
(()=TextendsX?1:2)extends
(()=TextendsY?1:2)
?A:B;
首先IfEqualsX, Y, A, B的四个参数,X和Y是待比较的两个类型,如果相等则返回A,不相等返回B。IfEquals的基本骨架是type IfEquals = (参数1) extends (参数2) ? A : B这样的,就是判断如果参数1的类型能够分配给参数2的类型,则返回A,否则返回B;参数1和参数2的基本结构是一样的,唯一区别在于X和Y不同。这里看下具体下面的例子://A=T()=Textendsstring?1:
typeA=T()=Textendsstring?1:2;
//B=T()=Textendsnumber?1:
typeB=T()=Textendsnumber?1:2;
//C=2
typeC=AextendsB?1:2;
是不是很奇怪,为什么能推导出A和B类型是不一样的?告诉你答案:
这是利用了Ts编译器的一个特点,就是Ts编译器会认为如果两个类型(比如这里的X和Y)仅被用于约束两个相同的泛型函数则是相同的。这理解起来有些不可思议,或者说在逻辑上这种逻辑并不对(因为可以举出反例),但是Ts开发团队保证了这一特性今后不会变。可参考这里。注意,这里也会判断的属性修饰符,例如readonly, 可选属性等,看通过下面的例子验证:/**
*T2比T1多了readonly修饰符
*T3比T1多了可选修饰符
*这里控制单一变量进行验证
*/
typeT1={key1:string};
typeT2={readonlykey1:string};
typeT3={key1?:string};
//A1=false
typeA1=IfEqualsT1,T2,true,false
//A2=false
typeA2=IfEqualsT1,T3,true,false
IfEquals最后就是借助1和2来辅助判断(语法层面的),还有就是给A的默认值为X,B的默认值为never。最后,如果你是个爱(搞)钻(事)研(情)的小宝宝,你或许会对我发出灵魂拷问:判断类型是否相等(兼容)为什么不直接使用type IfEqualsX, Y, A, B = X extends Y ? A : B呢?既简单有粗暴(PS:来自你的邪魅一笑~)。答案,我们看下下面的示例:
typeIfEqualsX,Y,A,B=XextendsY?A:
/**
*还用上面的例子
*/
typeT1={key1:string};
typeT2={readonlykey1:string};
typeT3={key1?:string};
//A1=true
typeA1=IfEqualsT1,T2,true,false
//A2=true
typeA2=IfEqualsT1,T3,true,false
答案显而易见,对readonly等这些修饰符,真的无能无力了。夸爪Kill~~~
第3步,解析MutableKeys实现逻辑
MutableKeys首先约束T为object类型通过映射类型[P in keyof T]进行遍历,key对应的值则是IfEquals类型1, 类型2, P,如果类型1和类型2相等则返回对应的P(也就是key),否则返回never。而P其实就是一个只有一个当前key的联合类型,所以[Q in P]: T[P]也只是一个普通的映射类型。但是要注意的是参数1{ [Q in P]: T[P] }是通过{}构造的一个类型,参数2{ -readonly [Q in P]: T[P] }也是通过{}构造的一个类型,两者的唯一区别即使-readonly。
所以这里就有意思了,回想一下上面的第一步的例子,是不是就理解了:如果P是只读的,那么参数1和参数2的P最终都是只读的;如果P是非只读的,则参数1的P为非只读的,而参数2的P被-readonly去掉了非只读属性从而变成了只读属性。因此就完成了筛选:P为非只读时IfEquals返回的P,P为只读时IfEquals返回never。
所以key为非只读时,类型为key,否则类型为never,最后通过[keyof T]得到了所有非只读key的联合类型。OptionalKeysOptionalKeys提取T中所有可选类型的key组成的联合类型。
typeOptionalKeys={
[PinkeyofT]:{}extendsPickT,P?P:never
}[keyof
typeEg=OptionalKeys{key1?:string,key2:number}
核心实现,用映射类型遍历所有key,通过PickT, P提取当前key和类型。注意,这里也是利用了同态拷贝会拷贝可选修饰符的特性。利用{} extends {当前key: 类型}判断是否是可选类型。//Eg2=false
typeEg2={}extends{key1:string}?true:false;
//Eg3=true
typeEg3={}extends{key1?:string}?true:false;
利用的就是{}和只包含可选参数类型{key?: string}是兼容的这一特性。把extends前面的{}替换成object也是可以的。
增强PickPickByValue提取指定值的类型//辅助函数,用于获取T中类型不能never的key组成的联合类型
typeTypeKeys=T[keyof
/**
*核心实现
*/
typePickByValueT,V=PickT,
TypeKeys{[PinkeyofT]:T[P]extendsV?P:never}
/**
*@example
*typeEg={
*key1:number;
*key3:number;
*}
*/
typeEg=PickByValue{key1:number,key2:string,key3:number},number
Ts的类型兼容特性,所以类似string是可以分配给string | number的,因此上述并不是精准的提取方式。如果实现精准的方式,则可以考虑下面个这个类型工具。
PickByValueExact精准的提取指定值的类型/**
*核心实现
*/
typePickByValueExactT,V=PickT,
TypeKeys{[PinkeyofT]:[T[P]]extends[V]
?([V]extends[T[P]]?P:never)
:never;
}
//typeEg1={b:number
typeEg1=PickByValueExact{a:string,b:number},number
//typeEg2={b:number;c:number|undefined}
typeEg2=PickByValueExact{a:string,b:number,c:number|undefined},number
PickByValueExact的核心实现主要有三点:
一是利用Pick提取我们需要的key对应的类型
二是利用给泛型套一层元组规避extends的分发式联合类型的特性
三是利用两个类型互相兼容的方式判断是否相同。
具体可以看下下面例子:
typeEq1X,Y=XextendsY?true:false;
typeEq2X,Y=[X]extends[Y]?true:false;
typeEq3X,Y=[X]extends[Y]
?([Y]extends[X]?true:false)
:false;
//boolean,期望是false
typeEg1=Eq1string|number,string
//false
typeEg2=Eq2string|number,string
//true,期望是false
typeEg3=Eq2string,string|number
//false
typeEg4=Eq3string,string|number
//true,非strictNullChecks模式下的结果
typeEg5=Eq3number|undefined,number
//false,strictNullChecks模式下的结果
typeEg6=Eq3number|undefined,number
从Eg1和Eg2对比可以看出,给extends参数套上元组可以避免分发的特性,从而得到期望的结果;从Eg3和Eg4对比可以看出,通过判断两个类型互相是否兼容的方式,可以得到从属类型的正确相等判断。从Eg5和Eg6对比可以看出,非strictNullChecks模式下,undefined和null可以赋值给其他类型的特性,导致number | undefined, number是兼容的,因为是非strictNullChecks模式,所以有这个结果也是符合预期。如果不需要此兼容结果,完全可以开启strictNullChecks模式。最后,同理想得到OmitByValue和OmitByValueExact基本一样的思路就不多说了,大家可以自己思考实现。
IntersectionIntersectionT, U从T中提取存在于U中的key和对应的类型。(注意,最终是从T中提取key和类型)
/**
*核心思路利用Pick提取指定的key组成的类型
*/
typeIntersectionTextendsobject,Uextendsobject=PickT,
ExtractkeyofT,keyofUExtractkeyofU,keyofT
typeEg=Intersection{key1:string},{key1:string,key2:number}
约束T和U都是object,然后利用Pick提取指定的key组成的类型通过Extractkeyof T, keyof U提取同时存在于T和U中的key,Extractkeyof U, keyof T也是同样的操作那么为什么要做2次Extract然后再交叉类型呢?原因还是在于处理类型的兼容推导问题,还记得string可分配给string | number的兼容吧。
扩展:
定义DiffT, U,从T中排除存在于U中的key和类型。
typeDiffTextendsobject,Uextendsobject=Pick
T,
ExcludekeyofT,keyofU
Overwrite 和 AssignOverwriteT, U从U中的同名属性的类型覆盖T中的同名属性类型。(后者中的同名属性覆盖前者)
/**
*Overwrite实现
*获取前者独有的key和类型,再取两者共有的key和该key在后者中的类型,最后合并。
*/
typeOverwrite
Textendsobject,
Uextendsobject,
I=DiffT,UIntersectionU,T
=PickI,keyofI
/**
*@example
*typeEg1={key1:number;}
*/
typeEg1=Overwrite{key1:string},{key1:number,other:boolean}
首先约束T和U这两个参数都是object借助一个参数I的默认值作为实现过程,使用的时候不需要传递I参数(只是辅助实现的)通过DiffT, U获取到存在于T但是不存在于U中的key和其类型。(即获取T自己特有key和类型)。通过IntersectionU, T获取U和T共有的key已经该key在U中的类型。即获取后者同名key已经类型。最后通过交叉类型进行合并,从而曲线救国实现了覆盖操作。扩展:如何实现一个AssignT, U(类似于Object.assign())用于合并呢?
//实现
typeAssign
Textendsobject,
Uextendsobject,
I=DiffT,UIntersectionU,TDiffU,T
=PickI,keyofI
/**
*@example
*typeEg={
*name:string;
*age:string;
*other:string;
*}
*/
typeEg=Assign
{name:string;age:number;},
{age:string;other:string;}
想一下,是不是就是先找到前者独有的key和类型,再找到两者共有的key以及该key在后者中的类型,最后找到后者独有的key和类型,最后依次的合并进去。
DeepRequiredDeepRequired将T的转换成必须属性。如果T为对象,则将递归对象将所有key转换成required,类型转换为NonUndefined;如果T为数组则递归遍历数组将每一项设置为NonUndefined。
/**
*DeepRequired实现
*/
typeDeepRequired=Textends(...args:any[])=any
?T
:TextendsArrayany
?_DeepRequiredArrayT[number]
:Textendsobject
?_DeepRequiredObject
:
//辅助工具,递归遍历数组将每一项转换成必选
interface_DeepRequiredArrayextendsArrayDeepRequiredNonUndefined{}
//辅助工具,递归遍历对象将每一项转换成必选
type_DeepRequiredObjectTextendsobject={
[PinkeyofT]-?:DeepRequiredNonUndefinedT[P]
}
DeepRequired利用extends判断如果是函数或Primitive的类型,就直接返回该类型。如果是数组类型,则借助_DeepRequiredArray进行递归,并且传递的参数为数组所有子项类型组成的联合类型,如下:typeA=[string,number]
/**
*@description对数组进行number索引访问,
*得到的是所有子项类型组成的联合类型
*typeB=string|number
*/
typeB=A[number]
_DeepRequiredObject是个接口(定义成type也可以),其类型是Array;而此处的T则通过DeepRequired进行对每一项进行递归;在T被使用之前,先被NonUndefined处理一次,去掉无效类型。
如果是对象类型,则借助_DeepRequiredObject实现对象的递归遍历。_DeepRequiredObject只是一个普通的映射类型进行变量,然后对每个key添加-?修饰符转换成required类型。
DeepReadonlyArrayDeepReadonlyArray将T的转换成只读的,如果T为object则将所有的key转换为只读的,如果T为数组则将数组转换成只读数组。整个过程是深度递归的。
/**
*DeepReadonly实现
*/
typeDeepReadonly=Textends((...args:any[])=any)|Primitive
?T
:Textends_DeepReadonlyArrayinferU
?_DeepReadonlyArray
:Textends_DeepReadonlyObjectinferV
?_DeepReadonlyObject
:
/**
*工具类型,构造一个只读数组
*/
interface_DeepReadonlyArrayextendsReadonlyArrayDeepReadonly{}
/**
*工具类型,构造一个只读对象
*/
type_DeepReadonlyObject={
readonly[PinkeyofT]:DeepReadonlyT[P]
};
基本实现原理和DeepRequired一样,但是注意infer U自动推导数组的类型,infer V推导对象的类型。UnionToIntersection将联合类型转变成交叉类型。
typeUnionToIntersection=(Textendsany
?(arg:T)=void
:never
)extends(arg:inferU)=void?U:never
typeEg=UnionToIntersection{key1:string}|{key2:number}
T extends any ? (arg: T) = void : never该表达式一定走true分支,用此方式构造一个逆变的联合类型(arg: T1) = void | (arg: T2) = void | (arg: Tn) = void再利用第二个extends配合infer推导得到U的类型,但是利用infer对协变类型的特性得到交叉类型。参考内容Ts官网 https://www.typescriptlang.org/docs/handbook/utility-types.htmlutility-types https://github.com/piotrwitek/utility-types
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线