将兼容类型分配给已区分的联合类型时发生错误

时间:2018-11-08 16:07:09

标签: 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 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还是{{1 }}。他们不仅需要相同的确切类型,而且B可以很好地编译。

任何人都可以解释发生了什么事吗?

2 个答案:

答案 0 :(得分:1)

更新:2019年5月30日,TypeScript 3.5发行版应通过smarter union type checking解决。以下内容适用于3.4及以下版本:


此问题之前has been reported,本质上是编译器的限制。尽管您和我都知道{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}可分配给后者,但不能分配给前者。

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

就您而言,我也持怀疑态度;您是否真的要建立一个区分工会,其中 only 的区别属性不同?那是病态的情况。区分联合的预期用例是采用一组不同类型并添加单个属性以让您知道值是哪种类型。一旦开始添加其他属性,这些属性使可区分的类型真正不同,就必须放弃折叠联合的想法。


因此,假设您确实需要上面的非歧视性歧视工会,您该怎么办?最简单的方法是使用type assertion:告诉编译器您正在做什么:

function test(input: C): AOrB {
  return input as AOrB; // you know best
}

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

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};
}

您想出的那些方法或其他方法都应该起作用。

好的,希望能有所帮助;祝你好运!

答案 1 :(得分:0)

因此,让我们逐步完成此步骤。让我们删除AB作为接口,并删除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;
}

现在,以类似的方式,让我们摆脱CDAOrB

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'的对象不能分配给具有单个属性{{1}的对象}或具有单个属性'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'; 的初始化类型为value,因此编译器假定这是此变量现在包含的值的类型。在这种情况下,将{ kind: 'a' }分配给b属性将被视为非法。

相关问题