【问题标题】:Intersection types with Typescript与 Typescript 的交叉点类型
【发布时间】:2021-02-13 06:04:01
【问题描述】:

我有两个自定义类型

type TypeA = string | number;
type TypeB = string | boolean;

我使用上述方法创建了一个交集type

type Combined = TypeA & TypeB;

当然,Combined 类型将仅属于 string 类型,因为它是 TypeATypeB 之间唯一相交的类型。

但如果我更改 TypeBunion 并添加 Date 类型,我会得到一个意外的行为,例如: p>

type TypeA = string | number;
type TypeB = string | boolean | Date;

创建一个新的交叉点类型

type Combined = TypeA & TypeB;

如果我检查类型的签名,它看起来像这样

type Combined = string | (string & Date) | (number & Date)

我的问题是为什么会这样?这是预期的吗?我认为它会是 string 类型,因为它是 相交 的唯一 type

【问题讨论】:

  • 这可能是 TypeScript 中的一个错误,但 string & numberneverstring & Datestring & Date,但我看不出你可以为 string & Date 分配什么值。
  • @ExplosionPills 这是一个错误吗?在我看来(可能很奇怪)实际上交叉口不是空的。例如,考虑表达式Object.assign(0, new Date())...的结果...
  • @CRice 不确定,但这似乎只是 0。您也可以通过 Object.assign(0, '')
  • 是的,我认为它只是0Object.assign 只是在欺骗类型系统,因为它的签名设置为返回其参数类型的交集。产生的实际值不能用作Date 对象,正如其类型所暗示的那样。我仍然不完全相信结果应该是never,但我认为我可以通过一些原型操作获得一个既可用作数字又可用作日期的值。
  • 我只是说 TypeScript 将 string & numbertype 视为 never: typescriptlang.org/play?#code/…

标签: typescript types intersection


【解决方案1】:

这不是错误;它允许一个(未广为人知的)功能称为 brandedtagged 原语。


几乎不可能在运行时同时拥有 string(原语,typeof v === "string",而不是 String 包装对象)和 Date 的值 v。在某种意义上,string & Date 类型实际上与never 相同,因为您可以将 TypeScript 类型视为所有适当 JavaScript 值的集合。如果没有 string & Date 值,也没有 never 值,那么这些类型在逻辑上是等价的。

因此,如果编译器急切地将string & Date 之类的交集减少为never,这将是合理的。它已经为"a" & 0 等不兼容单元类型的交集和string & number 等不兼容原始类型的交集做到了这一点,如microsoft/TypeScript#31838 中实现的那样。那么,当我们将对象类型与基元相交时,为什么不会发生这种情况呢?


原因是允许一个名为品牌原语的功能,它模拟nominal typing 用于TypeScript 中的原语(在this FAQ entry 中提到)。

TypeScript 大多只有structural typingtype aliases;如果两种类型具有相同的结构但名称不同,它们是相同的类型。如果您通过类型别名给现有类型一个新名称,它们是相同的类型。通常这正是您想要的,但有时您想提出两种类型,虽然在运行时相同,但需要在您的代码中加以区分,因为您不希望开发人员意外混淆它们。

例如(这可能是一个愚蠢的例子):

type Username = string;
type Password = string;
declare function login(username: Username; password: Password): void;

在这里,我们希望确保编写调用 login 的 TypeScript 代码的人不会意外输入用户名的密码,反之亦然。如果上述类型别名实际上阻止了您这样做,那就太好了:

declare function getUsername(): Username;
declare function getPassword(): Password;
login(getPassword(), getUsername()); // no error, OOPS

但事实并非如此。 UsernamePassword 类型都只是 string。我们使用不同的名称这一事实并没有改变这一点。因此,有时 TypeScript 开发人员希望他们的类型是名义上的,以捕捉上述错误。

microsoft/TypeScript#202 中有一个关于如何获得名义打字的很长的讨论。对原始类型执行此操作的一种方法是使用“品牌”,您可以在其中添加仅存在于类型系统中而不存在于运行时的“幻像”区分属性。因此,您可以将以上内容更改为:

type Username = string & { __brand: "Username" };
type Password = string & { __brand: "Password" };

突然你会在这里得到想要的错误:

login(getPassword(), getUsername()); // error! Password not assignable to Username

当然,实际上要让编译器相信特定的string 确实是UsernamePassword 是通过type assertion 之类的东西撒谎

function toUsername(x: string): Username {
    return x as Username; // <-- lying
}

但我们当然知道,在运行时你不能真的拥有UsernamePassword 类型的值,因为如果你采用原始的string 它不会有__brand 属性。如果编译器决定急切地将这些在运行时不可能的标记类型减少到never,它们将完全崩溃。这会比只使用原语更糟糕,因为没有任何东西可以分配给它们,但它们仍然无法区分并允许混淆:

login(getPassword(), getUsername()); // no error again

虽然此功能可能不是很有趣,但它已在现有的真实 TypeScript 代码中使用,包括 TypeScript source code for the TypeScript compiler itself。将品牌基元减少到 never 会破坏太多人,不值得。


Playground link to code

【讨论】:

  • @jpaugh 请注意this edit 不准确。 unit type 只包含一个值。所以像"a" 这样的字符串字面量类型和像0 这样的数字字面量类型是单位类型(以及undefinednull)。 stringnumber 类型不是单位类型,因为 "a""b" 都是 string 类型的有效值。而stringnumberprimitive types
  • 我明白了。感谢您花时间解释。我将添加一个小修改,以帮助我更好地理解它。
【解决方案2】:

这是预期的行为,原语的交集被简化为never,而原语与对象类型的交集没有被简化(以启用诸如品牌原语类型之类的东西)。 Date 不是原语,它是 lib.d.ts 中定义的对象类型

给出这个,并且打字稿通过移动我们得到的交集来规范化并集和交集的事实

type TypeA = string | number;
type TypeB = string | boolean;

type Combined = TypeA & TypeB;
 => (string | number) & (string | boolean)
 // Distributivity kicks in 
 => (string & string) | (string & boolean) | (number & string) | (number & boolean)
 //  intersection simplification
 => string | never | never | never 
 // never melts away in a union
 => string 

而在第二种情况下我们得到

type TypeA = string | number;
type TypeB = string | boolean | Date;

type Combined = TypeA & TypeB;
 => (string | number) & (string | boolean | Date)
 // Distributivity kicks in 
 => (string & string) | (string & boolean) | (string & Date) | (number & string) | (number & boolean) | (number & Date)
 //  intersection simplification, but nothing is done about Date and any primitive
 => string | never | (string & Date) never | never | (number & Date)
 // never melts away in a union
 => string 

如果您想从TypeA 中提取TypeB 中存在的任何类型,最好使用Extract 条件类型

type TypeA = string | number;
type TypeB = string | boolean | Date;

type Combined = Extract<TypeA, TypeB>;

Playground Link

【讨论】:

    【解决方案3】:

    那是因为 Date 不是原始类型。不兼容的原始类型减少到never

    type TypeA = string | number;
    type TypeB = string | boolean;
    
    type Combined = TypeA & TypeB;
    // string | (string & number) | (string & boolean)
    // => string | never | never
    // => string
    

    如果没有减少,开发人员更容易调试。 您可以尝试使用自己的类,它的行为将相同。

    Playground

    我找了一个更详细的答案,找到了这个:https://stackoverflow.com/a/53545038/14438744

    【讨论】:

      猜你喜欢
      • 2020-01-03
      • 1970-01-01
      • 2021-10-18
      • 2018-03-22
      • 1970-01-01
      • 2017-01-25
      • 2020-01-03
      • 1970-01-01
      • 2017-06-12
      相关资源
      最近更新 更多