TS类型体操技巧总结
点击关注公众号,”技术干货”及时达!最近刷类型体操有点上瘾,本文算是最近刷类型体操的一些思考的集合。刷的时候感觉自己长脑子了,但是过段时间又好像没脑子了,还是得通过博客总结沉淀一下
判断是否是 never 类型typeIsNever=[T]extends[never]?true:false;
typeIsNever=(()=T)extends()=never?true:false;
核心问题是 never extends never 返回是 never,之所以会这样是因为这里触发了 union 分配特性,左边的 never 可以视为一个空 union,使用元组或者函数包装一下都能正确判断(阻止触发分配特性)。更多细节建议移步:Generic conditional type T extends never ? 'yes' : 'no' resolves to never when T is never
typeIsNever=Textendsnever?true:false;
typeX=IsNevernever//=never
判断一个类型是否为 unknown错误做法:
typeIsUnknown=Textendsunknown?true:false;
typeX=IsUnknown2//true
unknown 类型可以理解为任意类型的父类,top 类型,比 Object 还 top。
反过来 extends 就可以:
typeIsUnknown=unknownextendsT?true:false;
和这个问题类似的还有判断一个类型是否为 number,如果要排除 number 字面量类型也应该反过来 extends:
typeIsNumber=numberextendsT?true:falsse;
typeX=IsNumber2//false
这类问题的共性就是要从一个类型中排除它子类,那就反过来 extends。
判断类型相等typeEqualsA,B=
(()=TextendsA?0:1)extendsT()=TextendsB?0:1?true:false;
多数人都会想到使用双向 extends 的方法,它能处理 「子类 extends 父类」 的情况,用元组包一下还能处理 never,但是处理不了 any:
typeEqualsA,B=[A]extends[B]?([B]extends[A]?true:false):false;
typeA=Equalsany,'1'//=true
更多细节建议移步:typescript 怎么判断两个类型相等?
表示键类型TS 有内置类型 PropertyKey:
typePropertyKey=string|number|symbol;
表示一个键值对麻烦的写法:
typeTupleToObjectTextends[any,any]={
[KinT[0]]:T[1];
};
typeX=TupleToObject['name','ly']//={name:"ly"}
可以直接用 Record:
typeTupleToObjectTextends[any,any]=RecordT[0],T[1]
交叉类型转接口类型麻烦的写法:
typeIntersectionToInterface={
[KinkeyofI]:I[K];
};
typeX=IntersectionToInterface{name:'ly'}{age:27}//={name:"ly";age:}
简写:
typeIntersectionToInterface=OmitI,never
由此,可以可以把 Merge 类型写的非常简单:
typeIntersectionToInterface=OmitI,never
typeMergeA,B=IntersectionToInterfaceOmitA,keyofBB
typeA={
name:'ly';
age:27;
};
typeB={
name:'YuTengjing';
height:170;
};
typeC=MergeA,B
/*
typeC={
name:"YuTengjing";
age:
height:
}
*/
类型映射可以使用 as 对 key 进行过滤例如不用内置的 Exclude 实现一个 MyOmit 类型:
typeMyOmitT,LextendskeyofT={
[KinkeyofTasKextendsL?never:K]:T[K];
};
typeX=MyOmit{name:'ly';age:27},'name'
/*
typeX={
age:
}
*/
类型映射对数组类型也是适用的曾经有段时间,我一直以为类型映射只能映射对象类型。
typeNumsToStrsArrextendsreadonlynumber[]={
[KinkeyofArr]:`${Arr[K]}`;
};
typeStrs=NumsToStrs[1,2,3]//=typeStrs=["1","2","3"]
ThisType使用 ThisType 这个内置类型,可以让我们给一个 interface 的方法的 this 混入其它属性。它并不是由其它类型生成的一个工具类型,是一个用于 TS 类型推断的标记类型。查看其源码:
/**
*Markerforcontextual'this'type
*/
interfaceThisType{}
一个非常贴近我们前端开发的应用:使用 ThisType 在 computed 和 methods 的方法中混入 data。
typeComputedCextendsRecordstring,any={
[KinkeyofC]:ReturnTypeC[K]
};
declarefunctionSimpleVueD,CextendsRecordstring,any,M(options:{
data:(this:{})=D;
computed:CThisTypeComputedDM
methods:MThisTypeMDComputed
}):DComputedCM;
SimpleVue({
data(){
return{
firstname:'Type',
lastname:'Challenges',
amount:10,
},
computed:{
fullname(){
return`${this.firstname}${this.lastname}`;
},
},
methods:{
hi(){
alert(this.amount);
alert(this.fullname.toLowerCase());
alert(this.getRandom());
},
},
});
infer + extends在模式匹配的时候可以使用 extends 来限定 infer 推断出的类型
模板字符串中匹配数字//TS没有NaN字面量类型
typeStringToNumberSextendsstring=Sextends`${inferNumextendsnumber}`?Num:never;
typeA=StringToNumber''//=never
typeB=StringToNumber'1'//=1
typeC=StringToNumber'1.2'//=1.2
在对元组使用模式匹配时能正确识别成员类型typeCount
Numsextendsreadonlynumber[],
Numextendsnumber,
Resultextendsreadonlyunknown[]=[],
=Numsextends[inferFirst,...inferRestextendsreadonlynumber[]]
?FirstextendsNum
?CountRest,Num,[...Result,unknown]
:CountRest,Num,Result
:Result['length'];
typeX=Count[1,2,2,3],2//=typeX=
默认情况下 TS 推断上面 Rest 类型为 unknown[],可以使用 extends 让 TS 只匹配时 readonly number[] 的情况。
数组参数推断为元组场景:我们要实现 Promise.all 方法的返回值推断
declarefunctionPromiseAllTextendsreadonlyany[](values:T):PromiseGetReturnT;
typeGetReturnTextendsreadonlyany[]=Textendsreadonly[inferFirst,...inferRest]
?[AwaitedFirst,...GetReturnRest]
:Textends[]
?[]
:TextendsArrayinferE
?ArrayAwaited
:
constR=PromiseAll([1,2,3]);//constR:Promisenumber[]
理想的推断结果为 Promise1, 2, 3。这里的核心问题在于,按照我们定义函数参数 values 的方式,推出的 values 是 number[] 类型。
解构一次declarefunctionPromiseAllTextendsreadonlyany[](
values:readonly[...T],
):PromiseGetReturnT;
//=constR:Promise[number,number,number]
通过将类型参数 T 解构一次来提示 TS 编译器将 values 尽可能推断为元组,但是这种方式没法将 values 推断为字面量类型。
常量泛型参数TS 5.0 引进的一个新语法:const Type Parameters,允许你对泛型参数标记为 const,这样 TS 在对函数参数推断时会直接将参数推断为字面类型:
declarefunctionPromiseAllconstTextendsreadonlyany[](values:T):PromiseGetReturnT;
constR=PromiseAll([1,2,3]);
//=constR:Promise[1,2,3]
判断是否为 unionIsUnion
充分利用了 union 有多个成员的特性,非 union 可以理解为只有一个成员的 union。
typeIsUnionU,E=U=[EextendsU?ExcludeU,E:never]extends[never]?false:true;
Union 转 IntersectiontypeUnionToIntersection=(Uextendsunknown?(arg:U)=void:never)extends(
arg:inferI,
)=void
?I
:never;
typeX=UnionToIntersection{name:'ly'}|{age:18}//={name:'ly'}{age:18}
U extends unknown ? (arg: U) = void : never 将 U 映射为一个函数 union:(arg: { name: 'ly' }) = void | (arg: { age: 18 }) = void函数 union 和 (arg: infer I) = void 进行模式匹配,这里函数 union 不会触发分配特性由于是 infer I 匹配多个处在逆变位置的参数,会取交叉类型取 Union 最后一项typeLastOfUnionU,FU=UnionToIntersectionUextendsunknown?()=U:never=Fextends(
...args:any[]
)=any
?ReturnType
:never;
typeX=LastOfUnion'a'|'b'|'c'//c
首先将 U 转为函数 union,通过 U extends unknown ? () = U : never 得到 () = 'a' | () = 'b'通过 UnionToIntersection 将上一步结果转换为函数交叉类型:() = 'a' 'b'函数交叉类型在模式匹配时和函数重载一样都是取最后一个函数用于匹配,也就是说这里 ReturnType() = 'a' 'b' 等同于 ReturnType() = 'b',返回 'b'declarefunctionfoo(x:string):number;
declarefunctionfoo(x:number):string;
declarefunctionfoo(x:string|number):string|number;
typeR=ReturnTypetypeoffoo//typeR=string|number
typeP=Parameterstypeoffoo//=typeP=[x:string|number]
由此我们可以结合递归来解答一道 hard 题:
typeUnionToTupleU,Last=LastOfUnion=[U]extends[never]
?[]
:[...UnionToTupleExcludeU,Last,Last];
typeTP=UnionToTuple'a'|'b'
//=['a','b]
递归在 TS 体操中,递归的使用率非常高。递归按照用途可以分为两类:循环 和 化为子问题。
循环TS 类型空间没有正儿八经的循环工具,但是我们可以通过递归来实现做循环,通过增加辅助泛型参数来暂存中间计算结果。递归实现循环有下面两种常见方式:
递归 + 下标 来循环:
typeJoin
Strsextendsreadonlystring[],
Indexextendsunknown[]=[],
Resultextendsstring='',
=Index['length']extendsStrs['length']
?Result
:JoinStrs,[...Index,unknown],`${Result}${Strs[Index['length']]}`
typeX=Join['a','b','c']//='abc'
递归 + 模式匹配 来循环:
typeJoinStrsextendsreadonlystring[],Resultextendsstring=''=Strsextends[
inferFirstextendsstring,
...inferRestextendsreadonlystring[],
]
?JoinRest,`${Result}${First}`
:Result;
我喜欢用第二种方式来循环,方便获取第一个元素,不用到处写 length。
化为子问题这才是真正是用递归化大问题为小问题,用递归的思路来解决问题:
typeJoinStrsextendsreadonlystring[]=Strsextends[
inferFirstextendsstring,
...inferRestextendsreadonlystring[],
]
?`${First}${JoinRest}`
:'';
排列组合问题实现排列组合的手段extends 左侧类型是 union 具有分配特性,有些文章翻译为分布式特性,我叫分配特性是因为它和乘法分配律很像对元组使用 number 类型索引返回的是成员的 union模板字符串参数为 union 会返回所有组合在元组中对数组 union 解构,返回的是数组的 union//1
typeNumberToStringUextendsnumber,E=U=EextendsU?`${E}`:never;
typeX=NumberToString1|2|3//=typeX="1"|"2"|"3"
//2
typeMembers=[1,2,3,4][number];//=typeMembers=4|1|2|3
//3
typeS=`${'a'|'b'}${'c'|'d'}`;//typeS="ac"|"ad"|"bc"|"bd"
//4
typeArr=[1,...([2]|[3])];//typeArr=[1,2]|[1,3]
灵活运用上面四个基本手段,可以解决大多数排列组合问题。
但是这些知识一些类型上的技巧,本质上解决一个类型体操问题还是需要找到问题的思路,多数体操问题可以用递归来解决问题。
实战解析全排列原题:permutation
递归思路:第一个坑位可以选取任意一个成员,然后对剩下的元素全排列,和第一个坑位组合的结果就是要的结果。
利用了手段 1 和 4,需要注意的是对 never 进行解构会导致整个数组返回 never 类型,这里 ExcludeU, E 最后会是 never,所以最开始需要判断是否为 never。
typePermutationU,E=U=[U]extends[never]
?[]
:EextendsU
?[E,...PermutationExcludeU,E]
:never;
typeX=Permutation'A'|'B'|'C'
//typeX=["A","B","C"]|["A","C","B"]|["B","A","C"]|["B","C","A"]|["C","A","B"]|["C","B","A"]
不去重的组合原题:combination
递归思路:组合要求至少有一个元素,那第一个坑位可以是任意一个成员,此时有两种选择,要和不和剩余元素组合,要么和剩下的元素的组合进行组合。
利用了手段 1, 2 和 3。
typeCombUextendsstring,E=U=EextendsU?`${E}${`${CombExcludeU,E}`|''}`:'';
typeCombinationTextendsreadonlystring[]=CombT[number]
typeX=Combination['foo','bar','baz']
//=typeX="foo"|"bar"|"baz"|"barbaz"|"bazbar"|"foobar"|"foobaz"|"foobarbaz"|"foobazbar"|"bazfoo"|"barfoo"|"barfoobaz"|"barbazfoo"|"bazfoobar"|"bazbarfoo"
元组全排列原题:permutations-of-tuple
测试用例:
Expect
Equal
PermutationsOfTuple[any,unknown,never],
|[any,unknown,never]
|[unknown,any,never]
|[unknown,never,any]
|[any,never,unknown]
|[never,any,unknown]
|[never,unknown,any]
这道题难点在于如果使用 number 索引元组返回得类型不对,你可能会想说先把数组 map 成每个成员被一个元组包围再去做全排列(也就是 [[any], [unknown], [never]]),我试过,很麻烦,而且还其它问题。所以这道题其实不适合用 number 去索引元组类型。
typeInsertTextendsreadonlyany[],U,E=T=EextendsT
?Textends[inferFirst,...inferRest]
?[U,...T]|[First,...InsertRest,U]
:[U]
:never;
typePermutationsOfTupleTextendsunknown[]=Textends[...inferFront,inferL]
?InsertPermutationsOfTupleFront,L
:
递归思路:例如我们求 PermutationsOfTuple[any, unknown, never],其实就是把最后的 never 想办法插入到 PermutationsOfTuple[any, unknown] 的结果中。
和这道题类似的还有另一道题:Transpose
typecases=[
ExpectEqualTranspose,[],
ExpectEqualTranspose[[1]],[[1]],
ExpectEqualTranspose[[1,2]],[[1],[2]],
ExpectEqualTranspose[[1,2],[3,4]],[[1,3],[2,4]],
ExpectEqualTranspose[[1,2,3],[4,5,6]],[[1,4],[2,5],[3,6]],
ExpectEqualTranspose[[1,4],[2,5],[3,6]],[[1,2,3],[4,5,6]],
ExpectEqualTranspose[[1,2,3],[4,5,6],[7,8,9]],[[1,4,7],[2,5,8],[3,6,9]],
];
翻译下这个问题:将矩阵顺时针旋转 90 度,或者说把列变成行
解法和上面类似,递归即可:求 Transpose[[1, 2, 3], [4, 5, 6], [7, 8, 9]],等同于求把 [7, 8, 9] 每一个元素插入到 Transpose[[1, 2, 3], [4, 5, 6] 的结果中
typeFallbackToT,Fallback=Textendsundefined?Fallback:
typeInsert
ArrextendsReadonlyArrayReadonlyArraynumber,
Rowextendsreadonlynumber[],
ResultextendsReadonlyArrayReadonlyArraynumber=[],
=Result['length']extendsRow['length']
?Result
:Insert
Arr,
Row,
[...Result,[...FallbackToArr[Result['length']],[],Row[Result['length']]]]
;
typeTransposeMextendsnumber[][]=Mextends[
...inferFrontextendsnumber[][],
inferLastextendsnumber[],
]
?InsertTransposeFront,Last
:
编码习惯良好的编码习惯可以让你的代码更易于让别人和自己理解。
泛型参数命名尽量取有意义的泛型参数名称,
字符串我们就用 S数字可以用 Num 或者干脆 N字符串数组可以用 Strs,数字数组可以用 Nums元组的第一个成员用 First,最后一个成员用 Last,infer 出来的 spread 数组用 Restunion 类型用 U成员是任意类型的数组可以用 Arr 或 List循环下标的数组可以用 Index结果可以用 Result 或者 Acc任意类型用 T任意的两个类型用 A 和 B总之不要一股脑用 T,T1 和 T2 这种。
typeTrimStartSextendsstring=any;
typeJoinUnion=any;
typeStrsToNumsStrsextendsreadonlystring[]=any;
typeGetLastArrextendsreadonlyunknown[]=Arrextends[...inferFront,inferLast]
?Last
:never;
在对 union 进行映射时,如果直接 U extends U,后序无法访问 union U,U 此时表示成员,原本的 union 会被 shadow。使用另一个泛型参数 E 保存 U 就没有这个问题,E 表示 Element。一方面是可读性更好,另一方面有时候确实需要访问原 union
//bad
typeNumsToStrsUextendsnumber=UextendsU?`${U}`:never;
//good
typeNumsToStrsUextendsnumber,E=U=EextendsU?`${E}`:never;
泛型约束「泛型约束」不仅是类型需求的一部分,也有助于理解类型。我们不但应该写泛型约束,还要遵循一定的最佳实践。
声明数组的时候尽量声明为「只读」的,因为声明为「可写」的数组那就不能接受「只读」的数组:
typeLengthArrextendsunknown[]=Arr['length'];
constarray=[1,2]asconst;
typeX=Lengthtypeofarray
/*
Type'readonly[1,2]'doesnotsatisfytheconstraint'unknown[]'.
Thetype'readonly[1,2]'is'readonly'andcannotbeassignedtothemutabletype'unknown[]'
*/
还有一个我思考过的问题:当你声明一个成员类型可以为任意类型的数组时,使用 readonly any[] 还是 readonly unknown[]?
大多数情况下两种声明方式都可以,少数情况下使用 readonly unknown[] 会达不到预期效果,具体案例我没印象了,等我想起来补一下。
长类型TS 实现逻辑毕竟不如 JS 那么方便,有些时候我们一个类型要写很长代码,为了便于理解不至于写到一半看不懂之前写的是啥我们编写的时候要注意:
按照标准格式去缩进必要时使用括号提高优先级抽离中间类型,性能一般还会更好适当增加辅助泛型参数关于第一点,其实主要想说的是 extends 缩进:
//?和:就不要写到一行,每次碰到extends就换行加缩进
AextendsB
?true
:false
关于第四点,还是拿之前的例子说明:
type_NumsToStrsUextendsnumber,E=U=EextendsU?`${E}`:never;
//如果题目要求只能有一个泛型参数,那我们直接alias下就好了
typeNumsToStrsUextendsnumber=__NumsToStrs
分享几道精妙的体操题Zip这道题就是说给定两个元组 A 和 B,返回一个元组 C, 满足:C[index] = [A[index], B[index]]
importtype{Equal,Expect}from'@type-challenges/utils';
typecases=[
ExpectEqualZip[],[],[],
ExpectEqualZip[1,2],[true,false],[[1,true],[2,false]],
ExpectEqualZip[1,2,3],['1','2'],[[1,'1'],[2,'2']],
ExpectEqualZip[],[1,2,3],[],
ExpectEqualZip[[1,2]],[3],[[[1,2],3]],
];
正常人的解法:
typeZip
Aextendsreadonlyany[],
Bextendsreadonlyany[],
Rextendsreadonlyany[]=[],
RLextendsnumber=R['length'],
=R['length']extendsA['length']|B['length']?R:ZipA,B,[...R,[A[RL],B[RL]]]
牛逼的解法,将模式匹配发挥到极致:
typeZipTextendsreadonlyany[],Uextendsreadonlyany[]=[T,U]extends[
[inferTF,...inferTR],
[inferUF,...inferUR],
]
?[[TF,UF],...ZipTR,UR]
:
Integer这道题判断给定类型是否为整数。
letx=1;
lety=1asconst;
typecases1=[
ExpectEqualInteger1,1,
ExpectEqualInteger1.1,never,
ExpectEqualInteger1.0,1,
ExpectEqualInteger1.0,1,
ExpectEqualInteger0.5,never,
ExpectEqualInteger28.0,28,
ExpectEqualInteger28.101,never,
ExpectEqualIntegertypeofx,never,
ExpectEqualIntegertypeofy,1,
];
正常人的解法
typeIntegerTextendsnumber=`${T}`extends`${inferIntextendsnumber}.${string}`
?never
:numberextendsT
?never
:
神的解法:
typeIntegerTextendsnumber=`${T}`extends`${bigint}`?T:never;
总结typescript 的类型空间有很多刻意为之的设计,例如 union extends none-union 会采用分配律返回 union,但是 union extends union 又会采用判断是否为子集返回 true/false,这些设计其实都有其实际意义,也使得 typescript 类型非常灵活和强大。
在做类型体操时,我们不应该将一开始就将思路集中到用什么类型技巧,解决问题的思路是通用的,无论是 JS 还是 TS 类型空间,核心还是思路。例如解决类型空间的斐波那契数列,你首先要明确你的思路,是自底向上动态规划去循环,还是自顶向下递归,明确了使用动态规划,那我们就再用类型工具去将思路具象化,使用两个数组的 length 去表示迭代变量。
类型体操玩归玩,别上头,平时工作,为了赶进度有些时候还是可以上 // @ts-expect-error 的,不建议直接 any。当我们时间不是那么紧迫的时候,其实可以稍微再类型声明上多花点时间提高代码的可维护性和可靠性。例如 method 不要直接声明为 string,而是声明为 union,使用模板字符串限制 url 必须以 http 开头等。
本文算是最近刷类型体操的一些思考的集合,后序还会慢慢补充一些内容。
最近我希望我会更新文章频繁一些,下一篇文章大概率是写前端工程化相关的。
点击关注公众号,”技术干货”及时达!
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线