TypeScript 中联合类型的类型推断

Type Inference for Union Types in TypeScript

假设我有三个基 类 或类型如下:

type A = { one: number };
type B = { one: number, two: string };
type C = { one: number, two: string, three: boolean };

现在我想创建一个类型化数组,其中包含这些类型的多个实例,因此我声明了一个 Union Type ,它将用于该数组:

type X = A | B | C;

在这里定义数组元素并构造数组:

const a: A = { one: 1 };
const b: B = { one: 1, two: '2' };
const c: C = { one: 1, two: '2', three: true };

let list: X[] = [a, b, c];

现在,如果我尝试访问此数组中第二个或第三个元素的 two 属性,我会收到以下错误:

const z: X = list[2];
z.two;                // yields this error
error TS2339: Property 'two' does not exist on type 'X'. Property 'two' does not exist on type 'A'.

我已经尝试更改 A 的类型 - 实际上还有其他人,从 const a: Aconst a: X,但我仍然遇到同样的错误。

如果我将 z: X 转换为 z: B;我仍然再次遇到非常相似的错误:

const z: B = list[2];
z.two;                // yields this error
Type 'X' is not assignable to type 'B'. Type 'A' is not assignable to type 'B'. Property 'two' is missing in type 'A'.

对我来说,TypeScript 的类型推断机制似乎确实以某种方式覆盖了我的显式键入,并且基本上没有创建 list: X[],而是创建了一个向下转换的版本,即 list: A[].

我也尝试过使用 classinterface 定义来查看它是否有所不同——尽管我确信它不会因为 TypeScript's Structural Type System,并且作为预计没有任何变化。

知道我在这里做错了什么或有任何关于更好方法的建议吗?

回答

原来将 const z: B = list[2] 更改为 const z = <B>list[2] 进行了我想要的正确转换。

您是否尝试过使用类型断言,例如

const z = <B>list[2]; // omitting the .two because that would never be assignable to B

?

它不会覆盖您的显式输入...它通过将 list 视为类型 X 的元素数组来 服从 您的显式输入。 X 可以是 ABC。如果我给你一个 X 类型的值,那么读取 one 属性 是安全的,因为它确实存在。但是尝试读取 two 属性 是 不安全的 ,因为 X 可能是 A,而 A 不知道有 two。所以你得到一个有用的错误:

z.two; // error!
// Property 'two' does not exist on type 'X'. Property 'two' does not exist on type 'A'.

那么,您有哪些选择?一种是告诉编译器你比它更了解,就像另一个答案中那样使用 type assertion

(z as B).two; // okay now

这会抑制编译器错误,但这确实不是一个很好的解决方案,因为它会在不需要时部分禁用类型检查。以下也不会是一个错误,但它会在 运行 时间给你带来问题:

(list[0] as B).two.length;  // no compile error
// at runtime: TypeError: list[0].two is undefined

通常,类型断言应该是处理编译器错误的最后手段,仅在您找不到合理的方法来说服编译器您正在做的事情是安全的,并且您确信它是安全的情况下使用安全并且即使面对可能的代码更改也将保持安全(例如,您将来将 list 更改为 [b,c,a])。


更好的解决方案是使用type guards让编译器相信你所做的是安全的。这会产生 运行 时间的影响,因为您 运行 正在编写更多代码,但如果您在某处更改 list,则您正在 运行 正在编写的代码更具前瞻性下线。这是一种方法:

const z = list[2]; // z is inferred as A | B | C
if ('two' in z) {
  // z is now narrowed to B | C
  z.two; // okay
}

因此,在使用 z.two 之前,您通过使用 in 检查 two 属性 是否存在来保护对 z.two 的读取。现在这是安全的。如果您正在考虑 "why should I go through this trouble when I know that z will be of type B (actually C, ha ha, list[2] is the third element)",请继续阅读:


如果您确定 list 将始终是类型为 ABC 的三元素数组,那么您可以告诉编译器这一点并在没有任何 运行 时间保护的情况下在编译时获得您期望的行为。您正在寻找 tuple types:

const list: [A, B, C] = [a, b, c];

const z = list[2];
z.two; // okay
z.three; // okay

您已经告诉编译器 list 是类型 [A, B, C] 的三元素 元组 。现在没有错误了(它可以看到 z 是一个 C)。这是安全的,零 运行 时间影响。它还可以防止您弄乱 list:

list[1] = a; // error!  A is not assignable to B

既然你已经告诉它 list[1] 总是一个 B,那么你必须给它分配一些与 B 兼容的东西:

list[1] = c; // okay, C is assignable to B

因此,您有三种选择:断言、类型保护和元组,对于这种情况,我推荐使用元组。希望有所帮助。祝你好运!