当前位置:

TSTypeScript 实践的 Equals 是工作的

访客 2024-04-24 1164 0

HowdoestheEqualsworkintypescript

循着线索慢慢来

在ts中如何判断两种类型完全一致?

三年前,在社区有一场关于支持typelevelequaloperator的讨论TypeScript#27024。

大佬@mattmccutchen给出了一个非常精彩的解决方案:

Here’sasolutionthatmakescreativeuseoftheassignabilityruleforconditionaltypes,whichrequiresthatthetypesafterextendsbe“identical”asthatisdefinedbythechecker:

exporttypeEquals<X,Y>=(<T>()=>TextendsX?1:2)extends(<T>()=>TextendsY?1:2)?true:false;

ThispassesallthetestsfromtheinitialdescriptionthatIwasabletorunexceptH,whichfailsbecausethedefinitionof“identical”doesn’tallowanintersectiontypetobeidenticaltoanobjecttypewiththesameproperties.(Iwasn’tabletoruntestEbecauseIdon’thavethedefinitionofHead.)

它本人并没有给出任何关于这个类型工作原理的解释,但它确实非常work,在实践中被大量使用。

不过,在后面其他人的交流中,发现了一些可能对理解有帮助的comment。

@fatcerberus

@jituanlinAFAIKitreliesonconditionaltypesbeingdeferredwhenTisnotknown.AssignabilityofdeferredconditionaltypesreliesonaninternalisTypeIdenticalTocheck,whichisonlytruefortwoconditionaltypesif:

  • Bothconditionaltypeshavethesameconstraint
  • Thetrueandfalsebranchesofbothconditionsarethesametype

这个类型在做的事情实际上就是,对<T>()=>TextendsX?1:2<T>()=>TextendsY?1:2做assignability检查。

而这个针对conditionaltype的检查,仅当下面两点满足时,才认为前者assignableto后者。

  • XY一致
  • conditionaltype各自的两个分支相应位置一致

但我不太确定他的所谓的“一致”(same)具体是什么含义。

后面,还有一条更有帮助的comment:

@tianzhich

@jituanlinAFAIKitreliesonconditionaltypesbeingdeferredwhenTisnotknown.AssignabilityofdeferredconditionaltypesreliesonaninternalisTypeIdenticalTocheck,whichisonlytruefortwoconditionaltypesif:

  • Bothconditionaltypeshavethesameconstraint
  • Thetrueandfalsebranchesofbothconditionsarethesametype

wherecanIfindtheinfomationsabouttheinternal‘isTypeIdenticalTo’check?Ican’tfindanythinginthetypescriptofficialwebsite…

Ifoundthisin/node_modules/typescript/lib/typescript.js,bysearchingisTypeIdenticalTo.Therearealsosomecommentsthatmayhelpsomeonehere:

//Twoconditionaltypes‘T1extendsU1?X1:Y1’and‘T2extendsU2?X2:Y2’arerelatedif
//oneofT1andT2isrelatedtotheother,U1andU2areidenticaltypes,X1isrelatedtoX2,
//andY1isrelatedtoY2.

ButI’mstillnotveryclearwhattherelatedmeanhere?Ican’tunderstandthesrccodeofisRelatedTo.

在hash[f1ff0de]-src/compiler/checker.ts中确实找到了这个注释:

它给出了对conditionaltype进行assignabilitycheck的更细致的说明:

它要求:

  • sourceType1和sourceType2只要存在任意方向的assignable关系即可。

    例如1number{foo:number,bar:string}{bar:string}都是可以的。

    Record<PropertyKey,unknown>和tupletype[]不行,stringnumber也不行。

  • extendFromType1和extendsFrom2必须是"完全一致"(identical)的。

  • canExtendBranchType1isassignabletocanExtendBranchType2。

  • cannotExtendBranchType1isassignabletocannotExtendBranchType2。

经过测试,注释中的‘related’指的是‘xassignabletoy’的关系。

这个关系过于细节,并非通过直觉就能推断出来的,所以Equals实际上是一个非常hack的实现。

好了,道理我都懂,Equals到底怎么工作的?

我们回头研究Equals的实现。

使用genericfunction的目的

我看到Equals的第一反应,是疑惑为什么长得这么怪,相等性判断为什么会跟函数扯上关系?

其实是否是函数并不重要,重要的是,我们需要在Equals的上下文中使用一个未被指定的generictypeT来构成一个conditionaltype。

实际上这个genericfunction从头到尾就没被实例化过,它的作用仅仅是提供一个可能为任意类型的genericT

注意,我这里提到的任意类型与any并不是一个概念,any基本上是所有类型的全集的概念,而任意类型则是全集中的任意集合的概念。

下文同。

所以,从这个角度来看,内部的conditionaltype与它在function中的位置并无关系,我们把它放在参数位置也是可以的:

typeEquals<X,Y>=(<T>(arg:TextendsX?1:2)=>any)extends(<T>(arg:TextendsY?1:2)=>any)?true:false;

conditionaltype是如何安排的

typeEquals<X,Y>=(<T>()=>TextendsX?1:2)extends(<U>()=>UextendsY?1:2)?true:false

第二个genericT换成U是为了提醒,两个conditionaltype中的T基本上无任何关系。

套用我们刚刚了解到的关于conditionaltype之间的assignability检查规则来看。

  • TU只要theonereatedtoother即可,而它们是任意类型,所以它们并不重要,也不需要考虑。

  • XY必须是完全一致的,这就是这个解决方案的核心hack点,利用tschecker对conditionaltype进行assignbilitycheck的机制,将XY放在正确的位置,从而让checker对XY进行了"完全一致"的这种相等性判断。

  • 至于12,它们只要满足对应位置上有正方向的assignable关系即可——即1extends12extends2

    所以12本身并不重要,我们可以根据上面的规则轻易构造出其他的例子。

    但还要注意的是,我们必须保证1位置上的类型notrelatedto2位置上的类型,才能让Equals在结果应该为false上的case也正常工作。

    例如下面这几个case都是work的:

    typeEquals1<X,Y>=(<T>()=>TextendsX?1:'1')extends(<U>()=>UextendsY?number:string)?true:falsetypeEquals2<X,Y>=(<T>()=>TextendsX?{foo:number}:2)extends(<U>()=>UextendsY?{foo:number,bar?:string}:{})?true:false;//这个case也是work的,想想为什么?typeEquals3<X,Y>=(<T>()=>TextendsX?T:T)extends(<U>()=>UextendsY?U:U)?true:false;

基本上就是这样,这应该是@mattmccutchen构造Equals时脑子里的冰山一角,更多的应该是TypeScript的实现,他对checker基本了如指掌才会有如此功力,而不是想我这样从注释中管中窥豹。

而即便如此,我也花了断断续续大约20有效思考小时,才勉强弄明白他的结构,以及各部分在这个功能中负责做什么。

还有一个可能对理解有帮助的来自爆栈网的解释,附在文末。

下面是用到的testcases,大家可以拿去自己把玩一下。

testcases

typeExcept<TextendsU,U>=TtypeHead<Textendsany[]>=Textends[inferF,...infer_]?F:never;typecases=[Except<Equals<1,2>,false>,Except<Equals<{foo:number},{foo:string}>,false>,Except<Equals<{foo:number},{foo:number,bar:string}>,false>,Except<Equals<{foo:number},{foo?:number}>,false>,Except<Equals<{foo:number},{foo:number}>,true>,Except<Equals<'a','a'|'b'>,false>,Except<Equals<never,never>,true>,Except<Equals<'a','a'>,true>,Except<Equals<string,number>,false>,Except<Equals<1,1>,true>,Except<Equals<any,1>,false>,Except<Equals<1|2,1>,false>,Except<Equals<Head<[1,2,3]>,1>,true>,Except<Equals<any,never>,false>,Except<Equals<never,any>,false>,Except<Equals<[any],[never]>,false>,]

ref

  1. Github-TypeScript#27024
  2. 爆栈网-HowdoestheEqualsworkintypescript?

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~