Typescript 将兼容类型指定给已识别的联合类型时出错

Typescript 将兼容类型指定给已识别的联合类型时出错,typescript,Typescript,我有一个简化的例子。我创建了一个有区别的联合AOrB,以及一个(在我看来)兼容的类型C: interface A { kind: 'a'; } interface B { kind: 'b'; } type Kind = 'a' | 'b'; interface C { kind: Kind; } interface D { kind: 'b'; } type AOrB = A | B; function test(input: C): AOrB { return

我有一个简化的例子。我创建了一个有区别的联合
AOrB
,以及一个(在我看来)兼容的类型
C

interface A {
  kind: 'a';
}

interface B {
  kind: 'b';
}

type Kind = 'a' | 'b';

interface C {
  kind: Kind;
}

interface D {
  kind: 'b';
}

type AOrB = A | B;

function test(input: C): AOrB {
  return input;
}

function test2(input: D): AOrB {
  return input;
}
失败于

Type 'C' is not assignable to type 'AOrB'.
  Type 'C' is not assignable to type 'B'.
    Types of property 'kind' are incompatible.
      Type 'Kind' is not assignable to type '"b"'.
        Type '"a"' is not assignable to type '"b"'.
这使得我似乎无法将对象分配给
AOrB
类型的变量,除非事先知道它实际上是
a
还是
B
。这不仅仅是因为它们需要相同的确切类型,因为
test2
可以很好地编译


有人能解释一下发生了什么吗?

让我们一步一步地完成这个过程。让我们删除
A
B
作为接口和
Kind
作为单独的类型,并用它们的精确值替换这些接口的所有出现:

interface C {
  kind: 'a' | 'b';
}

interface D {
  kind: 'b';
}

type AOrB = { kind: 'a' } | { kind: 'b' }

function test(input: C): AOrB {
  return input;
}

function test2(input: D): AOrB {
  return input;
}
现在,以类似的方式,让我们去掉
C
D
AOrB

function test(input: { kind: 'a' | 'b' }): { kind: 'a' } | { kind: 'b' } {
  return input;
}

function test2(input: { kind: 'b' }): { kind: 'a' } | { kind: 'b' } {
  return input;
}
现在,您可以在
test2
中看到,函数返回
{kind:'a'}{kind:'b'}
input
的类型为
{kind:'b'}
,因此它自然与该定义匹配

test1
中,函数返回
{kind:'a'}{kind:'b'}
,但输入类型为
{kind:'a''b'}
。虽然很容易说这些定义是匹配的,但是包含类型为
'a'
或类型为
'b'
的单个属性的对象不能分配给类型为
'a'
的单个属性的对象或类型为
'b'
的单个属性的对象。键入
{kind:'a'|'b'}
可以执行以下操作:

let value: { kind: 'a' | 'b' } = {
    kind: 'a'
};
value.kind = 'a';
value.kind = 'b';
现在,将
{kind:'a''b'}
替换为
{kind:'a'}{kind:'b'}

let value: { kind: 'a' } | { kind: 'b' } = {
    kind: 'a'
};
value.kind = 'a';
value.kind = 'b';

是用类型为
{kind:'a'}
的值初始化的,因此编译器假定该变量现在包含的值就是该类型的值。在这种情况下,将
b
分配给
种类
属性将被视为非法。

更新:2019-05-30随着TypeScript 3.5的发布,这一问题应通过解决。以下内容适用于3.4及以下内容:


这是以前的问题,本质上是编译器的限制。虽然您和我都知道
{a:X}{a:Y}
等同于
{a:X}{Y}
,但编译器并不这样做

通常,不能将对象的并集合并为并集对象,除非对象仅在一个属性的类型上不同(或者不同的属性表示所有可能的并集分布)。例如,您不能将
{a:X,b:Z}{a:Y,b:W}
折叠为类似
{a:X | Y,b:Z | W}
。。。类型
{a:X,b:W}
可分配给后者,但不能分配给前者

想象一下,编写编译器试图进行这样的缩减;在绝大多数情况下,它会花费宝贵的处理器时间检查联合类型,结果发现它无法将它们组合起来。像你这样相对罕见的情况下,减少可能是不值得的

即使在你的情况下,我也持怀疑态度;你真的在试图建立一个只有区别属性不同的区别联盟吗?那是一个病态病例。区分并集的预期用途是采用一组不同的类型并添加单个属性,以让您知道值是哪种类型。一旦您开始添加其他属性,使受歧视的类型真正不同,您就必须放弃折叠联合的想法


那么,假设你真的需要上面的无歧视工会,你能做什么?最简单的方法是使用以下命令告诉编译器您知道自己在做什么:

现在可以编译了,尽管它不是很安全:

function test(input: C): AOrB {
  return (Math.random() < 0.5 ? input : "whoopsie") as AOrB; // also compiles
}
无论是这些还是你想出的其他办法都应该管用


好吧,希望这会有帮助;祝你好运

我不会真的说{a:X}{a:Y}等同于{a:X}Y}——看我上面的分析。这确实是有趣的行为;编译器选择基于控制流缩小值类型的方式之间确实存在差异。尽管如此,考虑到语言在这个主题上的重要性,他们似乎都说“这些是等价的,但编译器不值得去确定这一点”,而不是“我们实际上希望这些类型的值得到不同的处理”。
function test(input: C): AOrB {
  return (Math.random() < 0.5 ? input : "whoopsie") as AOrB; // also compiles
}
function test(input: C): AOrB {      
  return input.kind === 'a' ? {kind: input.kind} : {kind: input.kind};
}