首页 前端知识 【TS】TypeScript 实践中的 Equals 是如何工作的?

【TS】TypeScript 实践中的 Equals 是如何工作的?

2024-05-26 00:05:21 前端知识 前端哥 732 730 我要收藏

How does the Equals work in typescript

desc

循着线索慢慢来

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

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

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

Here’s a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be “identical” as that is defined by the checker:

export type Equals<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

This passes all the tests from the initial description that I was able to run except H, which fails because the definition of “identical” doesn’t allow an intersection type to be identical to an object type with the same properties. (I wasn’t able to run test E because I don’t have the definition of Head.)

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

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

@fatcerberus

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

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

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

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

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

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

@tianzhich

@jituanlin AFAIK it relies on conditional types being deferred when T is not known. Assignability of deferred conditional types relies on an internal isTypeIdenticalTo check, which is only true for two conditional types if:

  • Both conditional types have the same constraint
  • The true and false branches of both conditions are the same type

where can I find the infomations about the internal ‘isTypeIdenticalTo’ check? I can’t find anything in the typescript official website…

I found this in /node_modules/typescript/lib/typescript.js, by searching isTypeIdenticalTo. There are also some comments that may help someone here:

// Two conditional types ‘T1 extends U1 ? X1 : Y1’ and ‘T2 extends U2 ? X2 : Y2’ are related if
// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,
// and Y1 is related to Y2.

image

But I’m still not very clear what the related mean here? I can’t understand the src code of isRelatedTo.

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

desc

它给出了对 conditional type 进行 assignability check 的更细致的说明:

它要求:

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

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

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

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

  • canExtendBranchType1 is assignable to canExtendBranchType2。

  • cannotExtendBranchType1 is assignable to cannotExtendBranchType2。

经过测试,注释中的 ‘related’ 指的是 ‘x assignable to y’ 的关系。

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

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

我们回头研究 Equals 的实现。

使用 generic function 的目的

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

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

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

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

下文同。

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

type Equals<X, Y> =
  (<T>(arg: T extends X ? 1 : 2) => any) extends
  (<T>(arg: T extends Y ? 1 : 2) => any) ? true : false;

conditional type 是如何安排的

type Equals<X, Y> =
	(<T>() => T extends X ? 1 : 2) extends
	(<U>() => U extends Y ? 1 : 2) ? true : false

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

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

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

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

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

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

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

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

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

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

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

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

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

test cases

type Except<T extends U, U> = T
type Head<T extends any[]> = T extends [infer F, ...infer _] ? F : never;

type cases = [
  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. 爆栈网 - How does the Equals work in typescript?
转载请注明出处或者链接地址:https://www.qianduange.cn//article/9527.html
标签
评论
发布的文章

html5怎么实现语音搜索

2024-06-01 10:06:32

HTML5

2024-02-27 11:02:15

HTML - 头部元素

2024-06-01 10:06:06

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!