【问题标题】:Shouldn't `string & any[]` result in `never`?`string 和 any[]` 不应该导致 `never` 吗?
【发布时间】:2018-11-30 02:02:18
【问题描述】:

我注意到 TypeScript 有一些奇怪的地方。我有一个类型联合,其中包含一些数组类型(string[]number[])和一些非数组类型(stringnumber)。如果我使用类型推断,一切都会按预期工作:

type bar = string | number | string[] | number[];
declare foo: bar;

if (Array.isArray(foo))
{
    foo // string[] | number[]
}
else
{
    foo // string | number
}

但是如果我想将类型直接限制为数组类型并使用类型交集,我会得到一些我没想到的东西:

declare foo: bar & any[];

// expected type: string[] | number[]

foo // (string & any[]) | (number & any[]) | (string[] & any[]) | (number[] & any[])

为什么会这样?
string & any[] 不应该评估为neverstring[] & any[] 评估为string[]

[link to playground]

【问题讨论】:

  • 您在bar & any[] 中没有类型保护,因此您的预期输出一开始就无法正常工作。
  • 抱歉,我正在尝试使用unknown 而不是any(这也不起作用)并且意外地将它留在那里。我修复了链接,所以现在操场代码等同于我问题中的代码。但是,我不知道您在说什么类型的守卫。我的问题中唯一的类型保护是Array.isArray
  • stringany[] 具有共同的 number 类型的键。
  • @MadaraUchiha 如果我们谈论的是类型联合,而不是交集,这将是相关的。类型联合允许我只访问公共键,并且是一个比使用的两种类型都宽松的类型。类型交集意味着变量必须具有stringany[] 的所有属性。而且似乎没有任何方法可以在 JavaScript 中实现这一点。

标签: typescript


【解决方案1】:

考虑到两个不重叠集的交集是空的直觉,完全不相交类型的交集应该评估为never 是合理的。这在不同时间都是requested before

事实上,自 TypeScript 2.6 以来,never 的减少一直是partially implemented。具体来说,像("a" | "b") & "c" 这样的类型会变成never,而"a" & "c" 不会。这样做是为了结合联合和交集不会导致巨大的联合类型。

但是引入此实现的拉取请求的描述为您的问题的答案提供了一些见解:“为什么编译器不一直这样做”?以下引述来自Anders Hejlsberg,TypeScript 语言的主要维护者/架构师之一。

这是他提到的一个问题:

理论上,我们可以更积极地删除空的交集类型,但我们不想破坏使用交集来“标记”原始类型的代码。

这种“标记”或“品牌”是在 TypeScript 中模拟 nominal typing 的一种方式。 TypeScript 使用structural typing 来比较类型,这意味着如果AB 具有相同的形状,编译器不会区分它们。有时,您希望能够使编译器以不同方式处理两种其他相同的类型(在 Java 等名义类型语言中的默认行为,其中只有 names AB足以区分类型)。好吧,如果您将其中一种类型与type AA = A & {randomPropName: any} 等额外属性相交,现在您可以将AAB 区分开来。这种品牌化在 TypeScript 编译器代码本身中很多是 mentioned,甚至是 used

因此,人们在某处依赖 string & {hoobydooby: true}string & {scoobydooby: false} 区分开来。如果这两个都减少到never,那么一切都会中断。所以他们不这样做。


他提到的另一个问题:

我们允许此类类型的存在主要是为了更容易发现它们的起源(例如,包含两个同名属性的对象类型的交集)。

因此,如果您有 {foo: string} & {foo: number} 这样的类型,则可以将其简化为 {foo: never} 甚至只是 never(毕竟,不应该存在 {foo: never} 类型的值),但我猜错误消息会变少可以理解:

interface A {foo: string}
interface B {foo: number}
type C = A & B;
const c: C = {foo: "hello"}; // error! string is not assignable to string & number;

这让您知道某些东西期望foo 既是字符串又是数字,这是不可能的,但指出您要调查C 类型。否则:

const c: C = {foo: "hello"}; // error! string is not assignable to never

我想这不太容易理解。

我个人认为这比第一个原因更弱,但它是您问题的“确定”答案的一部分。


编译器不执行开发人员希望它执行的操作还有其他原因;最普遍的原因是时间。即使您可以证明假设的编译器操作不会破坏任何人的代码并有助于您的用例,您也需要证明它不会严重损害编译器的性能。在这种情况下,编译器应该多积极地检查交叉点是否可能减少到never?如果A & B 通常不会减少到never,那么您对此的大部分检查都将是白费力气。所以检查最好很快。

事实证明,这个性能问题是功能提案和建议被拒绝或最终没有进入语言的一个非常常见的原因。我没有在任何关于这个特定问题的讨论中看到它,但如果它不是一个重要因素,我会感到非常惊讶。


好的,希望对您有所帮助。祝你好运!

【讨论】:

  • 太棒了,这是一个非常详尽的答案,谢谢!
  • 也许另一个原因是 TypeScript even in version 3.2 仍然使用类型联合而不是对象传播类型。这样,即使Object.assign({foo: 'a'}, {foo: 4}) 的结果不可用,至少它让你知道“foo 应该是字符串或数字,但我们不知道哪个先出现”。在此问题得到解决之前将string & number 更改为never 是不合理的做法。
【解决方案2】:

我认为您在这里看到的是联合类型的分布式特性。例如如果您将联合 A | B | CD 相交,则该交集将分发为 A & D | B & D | C & D

type U = A | B | C;
type Z = U & D; // distributes as A & D | B & D | C & D

第二个细微差别是交叉类型不会按照您的想法“折叠”(不确定此处的实际术语是什么)。一个更简单的例子是type X = Array<number> & Array<any>;。 X 类型不会折叠为Array<number>,但仍保持最初声明的交叉点。

值得注意的是,这似乎与交叉路口类型是否导致never 场景无关。例如,type X = { x: string } & { y: string }; 也保持声明状态,而不是显示为 { x: string, y: string }

编译器开始限制交集的部分是当您实际尝试分配不满足交集类型的内容时。

例如

type X = Array<number> & Array<any>; // this type will remain as declared
const x: X = ['some string']; // this will complain
const y: X = [4]; // this will work

如果你想限制你的类型,你可以使用条件类型而不是交集来过滤并集。

type Bar = string | number | string[] | number[];

declare const foo: Exclude<Bar, Array<any>>; // string | number

【讨论】:

  • 我认为Omit&lt;T,U&gt; 的标准名称是Exclude&lt;T,U&gt;
  • 我在特别问为什么交叉点类型没有像你所说的那样“折叠”。
  • 啊,我忘记了内置的 Exclude 类型 vs Omit,它通常用于过滤掉键。更新了我的答案。不幸的是,我不知道“为什么”的答案。
  • 我认为这与它是否永远不会发生有关。即使这种类型也不会“崩溃”type X = { x: string } &amp; { y: string };,即使人类可以将其推断为 { x: string, y: string }
猜你喜欢
  • 2022-01-03
  • 1970-01-01
  • 2020-07-29
  • 2018-08-07
  • 2017-04-14
  • 1970-01-01
  • 2017-06-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多