在 Typescript 中判断两个类型相等到底有多难?

前一阵在刷 TypeScript 类型体操的时候看到了 Equal 就一直在纠结这个问题,想尝试解释一下,如果有写的不对的地方欢迎指正,谢谢大家:D。

Typescript 中的类型相等

如果我们想判断两个变量是否相等,可以简单的通过 == 或 === 来进行比较,但是对比两个类型则不行。

在 TypeScript 中,类型是静态的,只会在编译时进行类型检查。

如果我们有两个类型 A 和 B,我们直接比较两个类型是否相等则会报错:

ts 复制代码
type A = number;
type B = string;
type C = number == string; // 'string' only refers to a type, but is being used as a value here.ts(2693)

我们可以看到 Typescript 提醒我们不能把类型当做值来用。

那我们如何判断两个类型是否相等呢?用的比较广泛的是 GitHub Feature requesttype level equal operator 中一位大神提到的 :

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

这段代码虽然很好用,但是原理却让人一头雾水,所以我尝试来分析下它到底是如何运作的。

条件类型

不熟悉 Typescript 类型的人可能不太清楚 T extends X ? 1 : 2 是什么意思,其实这是 TS 中的条件类型。

在 Typescript 中,有一个特性叫 "条件类型(Conditional Types)",条件类型的形式有点像 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression):

ts 复制代码
SomeType extends OtherType ? TrueType : FalseType;

当 extends 左边的类型可以赋值给右边的类型时,我们会得到第一个分支的类型 TrueType,否则得到后面的类型 FalseType

举个例子:

ts 复制代码
interface Animal {
    live(): void;
}
interface Dog extends Animal {
    woof(): void;
}

type Example1 = Dog extends Animal ? number : string; // number
type Example2 = RegExp extends Animal ? number : string; // string

那么问题来了,既然条件类型判断的是一个类型是否能复制给另一个类型,那么如何去判断可以是否可以赋值呢?我们要了解下 Typescript 的可赋值性。

Typescript 的可赋值性

为了了解 Typescript 的可赋值性,我专门找了一篇文章,并翻译了一下:译TypeScript 的可赋值性 简单总结一下:

  1. 如果两个类型相等,那么他们是可以互相赋值的。

  2. 简单类型的可赋值性就是判断他们的是否相等。

  3. 对象类型的可赋值性,如果一个类型是另一个类型的超类型(即包含了另一个类型的所有成员),那么这个类型的值可以赋给另一个类型的变量。

    ts 复制代码
    var source: { a: number, b: string };
    var target: { a: number };
    target = source;
  4. 函数类型的可赋值性,函数类型的可赋值性比较复杂,它需要考虑函数的参数类型和返回类型。返回值要判断原函数的返回值要可以赋值给目标函数的返回值,而参数以逆变的方式进行比较,即判断目标函数的参数都可以赋值给原函数。可能参数的逆变不太好懂,可以举个例子来说下:

    ts 复制代码
    var source: (a: string) => void;
    var target: (a: unknown) => void;
    target = source; // should be an error, because:
    target(1); // oops, can't pass numbers to source

这篇文章还有好多内容还没有讲,作者最后说 可以在 GitHub 上的 TypeScript 仓库中的 src/compiler/checker.ts 文件中查看 checkTypeRelatedTo 函数。

现在我们分析类型相等的代码:

ts 复制代码
(<T>() => T extends X ? 1 : 2) 
    extends (<T>() => T extends Y ? 1 : 2)
    ? true : false;

可以看到它需要判断的是两个函数类型的可赋值性:(<T>() => T extends X ? 1 : 2) 是否可以给 extends (<T>() => T extends Y ? 1 : 2),而这两个函数没有入参,我们只需要考虑返回值是否可以赋值即可。即条件类型 T extends X ? 1 : 2 是否可以赋值条件类型 T extends Y ? 1 : 2。

条件类型的可赋值性

如何判断一个条件类型可以赋值给另一个条件类型呢?在 Typescript 的仓库中 src/compiler/checker.ts 我们找到了 checkTypeRelatedTo 函数:

这个函数用于检查源类型 source 是否与目标类型 target 相关,其中关系 relation 可以是 identityRelation(恒等关系),subtypeRelation(子类型关系),assignableRelation(可赋值关系),或 comparableRelation(可比较关系)。

然后在其中找到了关于条件类型的判断代码链接:

根据注释,如果有两个条件类型 T1 extends U1 ? X1 : Y1 和 T2 extends U2 ? X2 : Y2,它们被认为是相关的,需要满足以下条件:

  1. T1 和 T2 中的一个与另一个相关。这意味着 T1 可以赋值给 T2 或者 T2 可以赋值给 T1。
  2. U1 和 U2 是相同的类型。
  3. X1 可以赋值 X2 。
  4. Y1 可以赋值 Y2 。

对于源代码中的泛型 T 我们对其都没有任何约束,我理解他们两个是具有可赋值性的(这里我并不确定,仅主观猜测)。

到此,我们再看一下 IsEqual 代码:

typescript 复制代码
type IsEqual<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

此时我们可以发现,当 X 和 Y 相等,(<T>() => T extends X ? 1 : 2) 可以赋值给 (<T>() => T extends Y ? 1 : 2) ,所以 IsEqual 为 true,反之则为 false。

小结

TypeScript 类型越研究越发现有非常多的内容,包括本文讨论的 Equal 其实已经涉及到编译器相关源码,能够提出这个函数的人肯定是对 TS 非常了解才能举重若轻地写出这个有些 hack 的代码,不过还是希望 TS 官方早日能给出 Equal 函数,减轻大家的心智负担。:)

参考资料

相关推荐
孟健8 分钟前
Claude Code 同需求写两版对比:装不装 frontend-design 差多远?
前端·ai编程·claude
梦de天空一米16882 小时前
【一个重复id让游戏卡在62%,聊聊配置表检查】
前端
ryn483982 小时前
01 一个功能模块,怎么分层才不乱
前端·ai编程·claude
Amos_Web2 小时前
通过 Rspack 源码学 Rust(五):读懂 Result 与问号运算符的错误返回
前端
杨航 AI2 小时前
JEV的用法
java·前端·数据库
Amos_Web2 小时前
Rspack 源码解析(三十五):rspack_error 如何用 miette 渲染带源码定位的错误链
前端
去伪存真2 小时前
高敏资产场景下的大模型协同设计架构
前端·agent
修炼的dance2 小时前
OCR预处理测了108组,纠斜去噪有用,坑在顺序和力度
前端
疯华整猫2 小时前
Android 16 后台常驻适配:用 Trae 搭一套“前台服务 + 加急 Job”的可存活架构
前端
不可能片场3 小时前
AI视频成片 帧间差分把拼接缝照出来
前端·electron