为什么TypeScript会推导出'never'而不是交叉类型?

7

给出以下示例:

interface Data {
    name: string;
    value: number;
}

const data :Data = {
    name: 'name',
    value: 1,
}

const updateValue = (key: keyof Data, value: string | number): void => {
    data[key] = value;
};


Typescript显示以下错误:

link to ts-playgound

Type 'string | number' is not assignable to type 'string & number'.
  Type 'string' is not assignable to type 'string & number'.
    Type 'string' is not assignable to type 'number'.

这段话很清晰易懂。但是,如果我在接口中添加联合类型,就像这样:

type MultiType = 'x' | 'y';

interface Data {
    name: string;
    value: number;
    multiType: MultiType | null;
}

const data :Data = {
    name: 'name',
    value: 1,
    multiType: null,
}

const updateValue = (key: keyof Data, value: string | number): void => {
    data[key] = value;
};

link to ts-playgound

I get the following error:

Type 'string | number' is not assignable to type 'never'.
  Type 'string' is not assignable to type 'never'.

如果我使用交叉类型string & number & MultiType,TypeScript会接受它,但它也会接受never

这对我来说似乎不一致。这可能是一个错误吗?


哪一部分看起来不一致?接受 never 的事实? - Kewin Dousse
2个回答

14
"

string & number等同于neverstring & number & (MultiType | null)也是如此。没有既是string又是number的值,因此没有一个值满足string & numberstring & number & AnythingElse

目前,后者明确缩小为never,因为它包含这些等同于never类型的联合,这样留下来真的很丑陋。具体来说,编译器会将交集分布到联合上,所以

"
string & number & ('x' | 'y' | null)

变成

(string & number & 'x') | (string & number & 'y') | (string & number & null)

那种类型对人们来说并没有特别有启发性,因此编译器检查每个联合成员是否等同于never,并将类型简化为{{never}}。

never | never | never

这只是

never

正如你所看到的。


那么为什么string & number本身不立即缩减为never呢?原本的想法是它可以帮助人们理解代码中bug的来源,因为string is not assignable to numberstring is not assignable to never更有启发性。但不幸的是,在TS3.5及以下版本中,虽然string & number在可分配和可分配值方面等同于never,但编译器并不总是对它们进行相同的处理,这很令人困惑。因此,从TS3.6开始,string & number这样的空交集将被显式地缩减为never。一旦TS3.6发布,您上述的代码在两种情况下将表现相同,并且您将得到string | number is not assignable to never错误。

好的,希望能帮到你;祝你好运!


我可能需要再读几遍,但我猜我理解了这个 :-) 谢谢。我会使用“never”作为值的类型。 - AndiH
我有点困惑...为什么您要将value标记为类型never呢?这样做后,您将无法使用updateValue()函数,因为没有值是类型为never的。是的,您可以使用类型断言来执行updateValue("name","alice" as never),但这并不安全;“alice”不是类型为never的。实际上,您需要使用具有keyof和查找类型的通用函数,在此文档中查看如何使用属性设置器的“setProperty()”。 - jcalz
请查看此链接 获取一种解决此问题的方法。其中不涉及交叉或 never - jcalz
它不起作用是因为没有阻止某人调用 updateValue("name", 12345) 并将一个 string 值的属性设置为一个数字。编译器表示它不知道 value 是否适合写入到 data[key]。读取属性和写入属性对类型安全有不同的影响。如果您从 data[K1 | K2] 中读取,可以将其安全地保存到类型为 data[K1] | data[K2] 的变量中。但是,如果您写入 data[K1 | k2],则无法从类型为 data[K1] | data[K2] 的表达式中执行该操作。唯一安全的写入方式是 data[K1] & data[K2] - jcalz
请参阅此文档以获取有关TS3.5+中发生这种情况的原因和方法的更多信息。 - jcalz
显示剩余2条评论

1

两件事:

  • 逻辑上讲,TypeScript 不接受将 string | number 作为你的 value 参数类型,因为以下所有情况:

    • string | number 不能分配给 string,因为当 key'name' 时,number 不能分配给 string

    • string | number 不能分配给 number,因为当 key'value' 时,string 不能分配给 number(这两个情况在你的第一个示例中已经成立)

    • string | number 不能分配给 MultiType | null,因为当 key'multiType' 时,numberstring 都不能分配给 'x' | 'y' | null

然而,出于某种原因,在您的第一个示例中,TypeScript仅停留在第一个“错误”案例上,并给出了此错误(即使已经有两个错误)。

在第二种情况下,您可能会看到相同的错误消息,因为类型不兼容仍然存在,似乎推断深入一点,告诉您问题比这更深刻。至于为什么错误消息格式如此,我不是很清楚,我想这需要更深入地了解编译器如何推断这些事情。最终,编译器是正确的,但错误消息可能需要更清晰。


对于您的第二个问题,“never”类型文档中提到:

never类型是每种类型的子类型,可以分配给每种类型。

因此,您可以将您的value指定为never类型,并将其分配给您的data。因为它永远不会发生。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接