网站搜索

typescript - 如何避免折叠封装的联合类型


在我看来,当 TypeScript 恰好知道初始值时,它会折叠一个封装的联合类型(这是不希望发生的)。我不确定这是不是一个错误,但我想知道是否有办法避免它。

一个例子:

type Prime = 3|5|7;
type OrDonuts<T> = T | 'donuts';

function compare<T>(a: T, b: OrDonuts<T>) {
    return a == b;
}

let value: Prime;
compare(value, 3);
// OK!

value = 5;
compare(value, 3);
// TS2345: Argument of type '5' is not assignable to parameter of type 'OrDonuts<3>'

为了绕过这个错误,我必须明确地展开,例如说 value=5 as Prime;。

这是一个 bug、预期行为,还是我只是做错了?

(节点: 10.15, 类型脚本: 3.5.1)

答案#1

TypeScript 类型检查器执行控制流类型分析,这意味着它会努力确定运行时变量内部的值会发生什么,并会缩小这些变量的类型以匹配。具体的一种方式是,如果你有一个联合类型的变量或属性,并且编译器看到你给它赋了一个更具体类型的值,它会将变量的类型缩小到那个更具体的类型,至少在你为该变量赋其他不同的值之前都是如此。

这通常是很可取的,因为它支持像这样的用例:

let x: number | string = Math.random() < 0.5 ? "hello" : "goodbye"; // x is string now
if (x.length < 6) { // no error here
  x = 0; // the widest x can be is string | number, so this assignment is fine
}

请注意,即使你已经标注了x为number | string类型,编译器也能理解在初始赋值之后它肯定是string类型。因此,当你检查x.length时,它不会报错(如果x可能是number类型的话,它就会报错)。这种行为非常有用,以至于禁用它会导致大量实际的 TypeScript 代码出现问题。

不幸的是,它也导致了你在这里看到的行为。在将 5 分配给 value 之后,编译器会将 value 视为包含缩小类型 5 的变量,而不是 Prime。你始终可以将 value 扩展 为 Prime,但编译器不会自动这样做。它认为通过警告你正在调用被禁止的 compare(5 as 5, 3) 函数是在帮助你。

在这种情况下,唯一覆盖此行为的方法是使用类型断言,正如你所看到的。你可以在初始赋值时进行这种断言,或在调用 compare() 时进行:

let value2: Prime = 5 as Prime
compare(value2, 3); // okay

let value3: Prime = 5;
compare(value3 as Prime, 3); // okay

或者,你可以在调用 compare() 时手动指定泛型类型 T,这也可以用:

let value4: Prime = 5;
compare<Prime>(value4, 3); // okay

你可以选择任何这些选项。

我能找到的关于这个的最权威的文档来源是 Microsoft/TypeScript#8513,特别是这条评论。

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

代码链接

答案#2

这是联合类型的预期行为。通过声明

type Prime = 3|5|7; // it is either 3 OR 5 OR 7 but never all of them at the the same time

你告诉 ts 编译器 Prime 是其中一个值。现在你将 5 分配给类型化字段 value。

value = 5; // type Prime is now 5
compare(value, 3); // <T> is inferred by the ts-compiler as 5

为了修复这个问题,你必须使用 value:Prime 或者像你那样进行类型断言。

没有类型推断,你将能够传递 value,例如

value = 5;
compare<2>(value, 3); // Argument of type '5' is not assignable to parameter of type '2'.

或者如果你想以更通用的方式满足 compare 函数参数,比如

let test = 5;
compare(test, 3); // <T> is now number which 5 and 3 are.