Typescript 中的“Equals”如何工作?

jay*_*ubi 18 typescript

我发现一个Equals实用程序提到: https://github.com/microsoft/TypeScript/issues/27024#issuecomment-421529650

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

它可用于检查两种类型是否相等,例如:

type R1 = Equals<{foo:string}, {bar:string}>; // false
type R2 = Equals<number, number>; // true
Run Code Online (Sandbox Code Playgroud)

我很难理解它是如何工作的以及T表达式中的含义。

有人可以解释一下吗?

Ale*_*hin 31

首先我们添加一些括号

\n
export type Equals<X, Y> =\n    (<T>() => (T extends /*1st*/ X ? 1 : 2)) extends /*2nd*/\n    (<T>() => (T extends /*3rd*/ Y ? 1 : 2))\n        ? true \n        : false;\n
Run Code Online (Sandbox Code Playgroud)\n

现在,当您替换某些类型时,X第二Yextends关键字所做的基本上是在问一个问题:“类型的变量是否<T>() => (T extends X ? 1 : 2)可以分配给类型的变量(<T>() => (T extends Y ? 1 : 2))?换句话说

\n
declare let x: <T>() => (T extends /*1st*/ X ? 1 : 2) // Substitute an actual type for X\ndeclare let y: <T>() => (T extends /*3rd*/ Y ? 1 : 2) // Substitute an actual type for Y\ny = x // Should this be an error or not?\n
Run Code Online (Sandbox Code Playgroud)\n

您提供的评论的作者说

\n
\n

条件类型 <...> 的可分配性规则要求后面的类型extends与检查器定义的类型“相同”

\n
\n

这里他们谈论的是第一个和第三个extends关键字。仅当它们后面的类型(即和)相同时,检查器才允许将x分配给。如果你替换yXYnumber两者

\n
declare let x: <T>() => (T extends number ? 1 : 2)\ndeclare let y: <T>() => (T extends number ? 1 : 2)\ny = x // Should this be an error or not?\n
Run Code Online (Sandbox Code Playgroud)\n

当然这不应该是一个错误,因为有两个相同类型的变量。现在如果你numberXand stringfor代替Y

\n
declare let x: <T>() => (T extends number ? 1 : 2)\ndeclare let y: <T>() => (T extends string ? 1 : 2)\ny = x // Should this be an error or not?\n
Run Code Online (Sandbox Code Playgroud)\n

现在后面的类型extends不相同,所以会出现错误。

\n
\n

现在让我们看看为什么后面的类型extends必须相同才能分配变量。如果它们相同,那么一切都应该很清楚,因为你只有 2 个相同类型的变量,它们总是可以互相分配。至于另一种情况,请考虑我描述的最后一种情况,即Equals<number, string>。想象这不是一个错误

\n
declare let x: <T>() => (T extends number ? 1 : 2)\ndeclare let y: <T>() => (T extends string ? 1 : 2)\ny = x // Imagine this is fine\n
Run Code Online (Sandbox Code Playgroud)\n

考虑这个代码片段:

\n
declare let x: <T>() => (T extends number ? 1 : 2)\ndeclare let y: <T>() => (T extends string ? 1 : 2)\n\nconst a = x<string>() // "a" is of type "2" because string doesn\'t extend number\nconst b = x<number>() // "b" is of type "1"\n\nconst c = y<string>() // "c" is of type "1" because string extends string\nconst d = y<number>() // "d" is of type "2"\n\ny = x\n// According to type declaration of "y" we know, that "e" should be of type "1"\n// But we just assigned x to y, and we know that "x" returns "2" in this scenario\n// That\'s not correct\nconst e = y<string>() \n// Same here, according to "y" type this should be "2", but since "y" is now "x",\n// this is actually "1"\nconst f = y<number>()\n
Run Code Online (Sandbox Code Playgroud)\n

如果类型不是stringnumber,则情况类似,它们没有任何共同点,但更复杂。让我们尝试一下{foo: string, bar: number}forX{foo: string}for Y。请注意,这里X可分配给Y

\n
declare let x: <T>() => (T extends {foo: string, bar: number} ? 1 : 2)\ndeclare let y: <T>() => (T extends {foo: string} ? 1 : 2)\n\n// "a" is of type "2" because {foo: string} doesn\'t extend {foo: string, bar: number}\nconst a = x<{foo: string}>()\n\n// "b" is of type "1"\nconst b = y<{foo: string}>()\n\ny = x\n// According to type declaration of "y" this should be of type "1", but we just\n// assigned x to y, and "x" returns "2" in this scenario\nconst c = y<{foo: string}>()\n
Run Code Online (Sandbox Code Playgroud)\n

如果你切换类型并尝试{foo: string}forX{foo: string, bar: number}for Y,那么调用时又会出现问题y<{foo: string}>()。你可以看到,总是有一些问题。

\n

更准确地说,如果XY不相同,则总会有某种类型扩展其中一个,而不扩展另一个。如果你尝试使用这种类型,T你会发现毫无意义。实际上,如果您尝试分配y = x,编译器会给出如下错误:

\n
Type \'<T>() => T extends number ? 1 : 2\' is not assignable to type \'<T>() => T extends string ? 1 : 2\'.\n  Type \'T extends number ? 1 : 2\' is not assignable to type \'T extends string ? 1 : 2\'.\n    Type \'1 | 2\' is not assignable to type \'T extends string ? 1 : 2\'.\n      Type \'1\' is not assignable to type \'T extends string ? 1 : 2\'.\n
Run Code Online (Sandbox Code Playgroud)\n

由于总是有一种类型可分配给其中之一XY而不能分配给另一个,因此它被迫将 的返回类型x视为1 | 2不可分配给T extends ... ? 1 : 2,因为T可以扩展它...,也不能扩展它。

\n

这基本上就是这种Equals类型的归结,希望它或多或少清楚它是如何工作的。

\n
\n

UPD 2:我想添加另一个简单的示例,其中 na\xc3\xafve 相等检查失败,但Equals没有。让X = {a?: number}Y = {},然后

\n
type NaiveEquals<X, Y> = \n  X extends Y ? Y extends X ? true : false : false\n\ntype A = NaiveEquals<{a?: number}, {}> // true\ntype B = Equals<{a?: number}, {}> // false\n
Run Code Online (Sandbox Code Playgroud)\n

备注:如果你想细致一点,理论上A也应该是false,因为{} extends {a?: number}应该是false(并非所有类型的值{}都可以分配给类型的变量{a?: number})。但 TS 并不像它声称的那样“100% 健全”,所以在 TS 中这是true

\n

例如,这里的 type{a: string}可分配给{},但不能分配给{a?: number},因此当您使用它时会出现错误:

\n
declare let x: <T>() => (T extends {a?: number} ? 1 : 2)\ndeclare let y: <T>() => (T extends {} ? 1 : 2)\n\n// "a" is of type "2" because {a: string} doesn\'t extend {a?: number}\nconst a = x<{a: string}>()\n\n// "b" is of type "1"\nconst b = y<{a: string}>()\n\ny = x\n// According to type declaration of "y" this should be of type "1", but we just\n// assigned x to y, and "x" returns "2" in this scenario\nconst c = y<{a: string}>()\n
Run Code Online (Sandbox Code Playgroud)\n
\n

更新1:

\n

说说为什么Equals<{x: 1} & {y: 2}, {x: 1, y: 2}>false

\n

据我了解,这是一个实现细节(不确定我是否应该称其为错误,这可能是故意的错误行为、语言限制、性能权衡或其他)

\n

理论上当然应该是这样true。正如我上面所描述的,当且仅当存在可分配给 和 之一但不能分配给另一个的类型时,才返回(理论上Equals) 。在这种情况下,在上面的示例中,如果您这样做并将其粘贴在 (和) 中,您会得到错误的输入。然而,这里的情况并非如此,所有可分配给的东西都可以分配给,所以理论上应该返回falseCCXYx = yx<C>()y<C>(){x: 1} & {y: 2}{x: 1, y: 2}Equalstrue

\n

然而,实际上,在确定类型是否相同时,打字稿实现似乎采取了一种更懒的方法。我应该指出,这是一个猜测,我从未对打字稿做出过贡献,也不知道它的源代码,但这是我在过去 10 分钟内发现的,我可能完全错过一些细节,但这个想法应该是正确的。

\n

在 ts 存储库中进行类型检查的文件是checker.ts(该链接指向 ts 4.4 和 4.5 之间的文件版本,将来可能会更改)。这里的线似乎是比较和部分19130的地方。以下是相关部分:T extends X ? 1 : 2T extends Y ? 1 : 2

\n
// Line 19130\n// Two conditional types \'T1 extends U1 ? X1 : Y1\' and \'T2 extends U2 ? X2 : Y2\' are related if\n// one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2,\n// and Y1 is related to Y2.\n// ...\nlet sourceExtends = (source as ConditionalType).extendsType;\n// ...\n// Line 19143\nif (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) && /* ... */) {\n  // ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

注释说这些类型是相关的,如果除其他条件外,U1U2,在我们的例子中XY,是相同的,这正是我们要检查的。在网上,19143您可以看到正在比较之后的类型extends,这会导致isTypeIdenticalTo函数,该函数又调用isTypeRelatedTo(source, target, identityRelation)

\n
function isTypeRelatedTo(source: Type, target: Type, relation: /* ... */) {\n    // ...\n    if (source === target) {\n        return true;\n    }\n    if (relation !== identityRelation) {\n        // ...\n    }\n    else {\n        if (source.flags !== target.flags) return false;\n        if (source.flags & TypeFlags.Singleton) return true;\n    }\n    // ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n

您可以看到,它首先检查它们是否是完全相同的类型(就 ts 实现而言,这{x: 1} & {y: 2}不是{x: 1, y: 2}完全相同的类型),然后比较它们的flags. 如果您查看此处Type类型的定义,您会发现它的类型是此处定义的,并且您会查看该类型:交集有它自己的标志。所以有一个标志,没有,因此它们不相关,因此返回,尽管理论上它不应该返回。flagsTypeFlags{x: 1} & {y: 2}Intersection{x: 1, y: 2}Equalsfalse

\n

  • 但“T”与“X”或“Y”无关 (3认同)
  • 是的,我们不能向它传递任何东西。它只是一个中间的东西,我们用它作为工具来实现创建“Equals”类型的目标。并且“T”类型从未被分配特定的东西,它只是一个**通用**类型,稍后当函数被调用时它会被替换。但由于该函数从未被调用,因为我们仅使用该函数作为比较“X”和“Y”的中间手段,因此“T”类型从未分配任何具体类型。再说一次,这只是一个中间步骤,有助于比较“X”和“Y” (2认同)