在 typescript 中'等号'是如何工作的?

p5fdfcr1  于 2023-02-13  发布在  TypeScript
关注(0)|答案(1)|浏览(145)

我在以下位置找到了一个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;

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

type R1 = Equals<{foo:string}, {bar:string}>; // false
type R2 = Equals<number, number>; // true

我很难理解这是如何工作的,以及表达式中的T是什么意思。
有人能解释一下吗?

6gpjuf90

6gpjuf901#

首先,让我们添加一些括号

export type Equals<X, Y> =
    (<T>() => (T extends /*1st*/ X ? 1 : 2)) extends /*2nd*/
    (<T>() => (T extends /*3rd*/ Y ? 1 : 2))
        ? true 
        : false;

现在,当你用一些类型替换XY时,第二个extends关键字基本上是在问一个问题:“<T>() => (T extends X ? 1 : 2)类型的变量是否可赋值给(<T>() => (T extends Y ? 1 : 2))类型的变量?换句话说

declare let x: <T>() => (T extends /*1st*/ X ? 1 : 2) // Substitute an actual type for X
declare let y: <T>() => (T extends /*3rd*/ Y ? 1 : 2) // Substitute an actual type for Y
y = x // Should this be an error or not?

你提供的评论的作者说
条件类型的可赋值性规则<...>要求extends之后的类型与检查器定义的类型“相同
这里他们讨论的是第一个和第三个extends关键字,检查器只允许x可以赋值给y,前提是它们后面的类型,即XY,是相同的。

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends number ? 1 : 2)
y = x // Should this be an error or not?

当然这不应该是错误,因为有两个相同类型的变量,现在如果你用number替换X,用string替换Y

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)
y = x // Should this be an error or not?

现在extends之后的类型不相同,因此会出现错误。
现在让我们看看为什么extends后面的变量类型必须相同才能赋值。如果它们相同,那么一切都应该很清楚,因为你只有两个相同类型的变量,它们总是可以彼此赋值的。至于另一种情况,考虑我描述的最后一种情况,Equals<number, string>。假设这不是一个错误

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)
y = x // Imagine this is fine

请考虑以下代码片段:

declare let x: <T>() => (T extends number ? 1 : 2)
declare let y: <T>() => (T extends string ? 1 : 2)

const a = x<string>() // "a" is of type "2" because string doesn't extend number
const b = x<number>() // "b" is of type "1"

const c = y<string>() // "c" is of type "1" because string extends string
const d = y<number>() // "d" is of type "2"

y = x
// According to type declaration of "y" we know, that "e" should be of type "1"
// But we just assigned x to y, and we know that "x" returns "2" in this scenario
// That's not correct
const e = y<string>() 
// Same here, according to "y" type this should be "2", but since "y" is now "x",
// this is actually "1"
const f = y<number>()

如果类型不是stringnumber也是类似的,这两个类型没有任何共同点,但更复杂。让我们尝试将{foo: string, bar: number}指定为X,将{foo: string}指定为Y。注意,这里X可赋值给Y

declare let x: <T>() => (T extends {foo: string, bar: number} ? 1 : 2)
declare let y: <T>() => (T extends {foo: string} ? 1 : 2)

// "a" is of type "2" because {foo: string} doesn't extend {foo: string, bar: number}
const a = x<{foo: string}>()

// "b" is of type "1"
const b = y<{foo: string}>()

y = x
// According to type declaration of "y" this should be of type "1", but we just
// assigned x to y, and "x" returns "2" in this scenario
const c = y<{foo: string}>()

如果您切换类型,并尝试{foo: string}来替换X,尝试{foo: string, bar: number}来替换Y,那么调用y<{foo: string}>()将再次出现问题,您可以看到总是有错误。
更准确地说,如果XY不相同,总会有一些类型扩展其中一个,而不扩展另一个。如果你试图为T使用这种类型,你会得到一个无意义的。实际上,如果你试图为y = x赋值,编译器会给你一个如下的错误:

Type '<T>() => T extends number ? 1 : 2' is not assignable to type '<T>() => T extends string ? 1 : 2'.
  Type 'T extends number ? 1 : 2' is not assignable to type 'T extends string ? 1 : 2'.
    Type '1 | 2' is not assignable to type 'T extends string ? 1 : 2'.
      Type '1' is not assignable to type 'T extends string ? 1 : 2'.

因为总是有一个类型可以赋值给XY中的一个,而不能赋值给另一个,所以它被迫将x的返回类型视为1 | 2,而1 | 2不能赋值给T extends ... ? 1 : 2,因为T可以扩展这个...,也可以不扩展。
这基本上就是Equals类型的本质,希望它或多或少地清楚它是如何工作的。
UPD 2:我想添加另一个简单的例子,其中一个简单的相等性检查失败,但是Equals没有。

type NaiveEquals<X, Y> = 
  X extends Y ? Y extends X ? true : false : false

type A = NaiveEquals<{a?: number}, {}> // true
type B = Equals<{a?: number}, {}> // false

备注:如果你想更细致,理论上A也应该是false,因为{} extends {a?: number}应该是false(不是所有{}类型的变量都可以赋值给{a?: number}类型的变量),但是TS并不像它声称的那样“100%正确”,所以在TS中这是true
例如,类型{a: string}可赋值给{},但不能赋值给{a?: number},因此当您使用它时,会收到一个错误:

declare let x: <T>() => (T extends {a?: number} ? 1 : 2)
declare let y: <T>() => (T extends {} ? 1 : 2)

// "a" is of type "2" because {a: string} doesn't extend {a?: number}
const a = x<{a: string}>()

// "b" is of type "1"
const b = y<{a: string}>()

y = x
// According to type declaration of "y" this should be of type "1", but we just
// assigned x to y, and "x" returns "2" in this scenario
const c = y<{a: string}>()

UPD 1:
说到为什么Equals<{x: 1} & {y: 2}, {x: 1, y: 2}>false
tl;dr据我所知,这是一个实现细节(不确定我是否应该称之为bug,这可能是故意的)
当然,理论上应该是true,正如我前面所述,Equals返回false(理论上)当且仅当存在类型X1 M62 N1 X使得X1 M63 N1 X可分配给X1 M64 N1 X和X1 M65 N1 X之一,但另一个没有,在上面的例子中,如果你执行x = y并插入(x<C>()y<C>()),你会得到错误的输入。然而,在这里,情况不是这样的,所有可以赋值给{x: 1} & {y: 2}的东西都可以赋值给{x: 1, y: 2},所以理论上Equals应该返回true
然而,实际上,在判断类型是否相同时,typescript的实现似乎采取了一种更懒惰的方法。我应该指出,这是一种猜测,我从未对typescript做出过贡献,也不知道它的源代码,但这是我在过去10分钟内发现的,我可能完全错过了一些细节,但这个想法应该是正确的。
ts存储库中执行类型检查的文件是checker.ts(链接指向ts 4.4和4.5之间的文件版本,将来可能会更改)。此处的19130行似乎是比较T extends X ? 1 : 2T extends Y ? 1 : 2部分的位置。以下是相关部分:

// Line 19130
// 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.
// ...
let sourceExtends = (source as ConditionalType).extendsType;
// ...
// Line 19143
if (isTypeIdenticalTo(sourceExtends, (target as ConditionalType).extendsType) && /* ... */) {
  // ...
}

注解说这些类型是相关的,如果在其他条件中,U1U2,在我们的例子中XY,是相同的,这正是我们要检查的。在19143行,你可以看到extends后面的类型正在被比较,这导致isTypeIdenticalTo函数,它依次调用isTypeRelatedTo(source, target, identityRelation)

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

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

相关问题