网站搜索

typescript - 在类型缩小和带有泛型类型的 `Extract` 辅助函数交互中出现意外的类型错误


我对以下问题感到困惑。我有一个简单的标记联合:

type MyUnion = 
    | { tag: "Foo"; field: string; } 
    | { tag: "Bar"; } 
    | null;

现在我有这个通用函数:

const foo = <T extends MyUnion>(value: T) => {
    // Narrowing works as expected.
    if (value === null) { return; }
    const notNull: NonNullable<T> = value;

    // Narrowing works with field access, but not with `Extract` type.
    if (value.tag !== "Foo") { return; }
    const s: string = value.field;  // Field can be accessed -> all good here.
    const extracted: Extract<T, { tag: "Foo" }> = value;  // error!
};

(游乐场)

最后一行导致了这个错误:

Type 'T & {}' is not assignable to type 'Extract<T, { tag: "Foo"; }>'.

我真的无法理解为什么。在函数外部写成没有泛型的形式,即 Extract<MyUnion, { tag: "Foo" }>,结果正好是正确的类型。这里到底发生了什么?

注意:在我真实的代码中,该函数还有另一个参数 callback: (v: Extract<T, { tag: "Foo" }>) => void。而调用该 callback 会导致相同的类型错误。所以我的目标是以某种方式让这行得通:Playground。因此该函数 foo 基本上对一个值执行特定检查,以某种方式处理特殊情况,然后使用已清理的值调用 callback。我猜 TypeScript 以某种方式允许我表达这种抽象?

答案#1

这可以被视为 TypeScript 的一个设计限制。这个问题在于 Extract 是一个依赖于泛型类型 T 的条件类型。

T 在你使用条件类型的代码点几乎是未指定的。当然,T 有一个约束,强制它成为 MyUnion 的子类型。但尚未知道任何 具体 类型,因为它由函数的调用者指定。每当编译器遇到这样的条件类型时,它无法进行求值。该类型本质上保持不透明,直到只有 Extract<T, { tag: "Foo" }> 类型的值可以安全地赋给它为止。

你可以在 #33484 或 #28884 中找到关于类似问题的讨论。在很多关于泛型和条件的情况下,类型对阅读代码的人来说可能是合理的,但由于类型尚未解析,编译器会停止推理。


你可能想知道为什么这行得通:

const notNull: NonNullable<T> = value;

#49119 将 NonNullable 从一种 条件类型 改为仅将 T 与 {} 相交的类型。

- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};

同一个 PR 也引入了对控制流分析的改进。检查一个具有泛型类型的变量是否不为 null 会将该变量的类型与 {} 进行交集。

这使得可赋值成为可能,因为 NonNullable<T> 和 value 的类型都会计算为 T & {}