网站搜索

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"'.

这让人觉得,除非事先知道对象究竟是 A 还是 B,否则我无法将对象分配给类型为 AOrB 的变量。这并不是说它们必须完全相同类型,因为 test2 可以正常编译。

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

答案#1

更新: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 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};
}

那两者中的任意一个,或者你想出的其他东西,都应该可以。

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

答案#2

所以让我们一步一步来。让我们将 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';

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