泛型与Extract工具类型、类型收窄交互时的意外类型错误
问题原因
在泛型函数中,当T是MyUnion的子类型时,TypeScript无法自动将类型收窄后的值与Extract<T, { tag: "Foo" }>类型建立精确的兼容性关联。虽然value !== null && value.tag === "Foo"能让TypeScript识别当前value是MyUnion中的Foo类型,但在泛型上下文里,Extract<T, ...>是一个条件类型,TypeScript无法推断收窄后的T & { tag: "Foo"; field: string; }完全等同于Extract<T, { tag: "Foo" }>,从而抛出赋值/回调调用的类型错误。
解决方案
方案1:使用类型断言(快速解决)
既然已经通过条件判断确保了value的类型符合Extract<T, { tag: "Foo" }>,可以直接用类型断言告诉TypeScript:
type MyUnion = | { tag: "Foo"; field: string; } | { tag: "Bar"; } | null; function foo<T extends MyUnion>(value: T) { if (value !== null && value.tag === "Foo") { console.log(value.field); // 正常访问 // 赋值时断言 const fooValue: Extract<T, { tag: "Foo" }> = value as Extract<T, { tag: "Foo" }>; // 调用回调时断言 const callback: (v: Extract<T, { tag: "Foo" }>) => void = (v) => console.log(v.field); callback(value as Extract<T, { tag: "Foo" }>); } }
方案2:自定义类型守卫(更安全的类型推导)
写一个专门的类型守卫函数,让TypeScript明确知道条件成立时value就是Extract<T, { tag: "Foo" }>类型:
type MyUnion = | { tag: "Foo"; field: string; } | { tag: "Bar"; } | null; // 自定义泛型类型守卫 function isFoo<T extends MyUnion>(value: T): value is Extract<T, { tag: "Foo" }> { return value !== null && value.tag === "Foo"; } function foo<T extends MyUnion>(value: T) { if (isFoo(value)) { // TypeScript自动推断value为Extract<T, { tag: "Foo" }>类型 console.log(value.field); const fooValue: Extract<T, { tag: "Foo" }> = value; // 无错误 const callback: (v: Extract<T, { tag: "Foo" }>) => void = (v) => console.log(v.field); callback(value); // 无错误 } }
这种方式不需要手动断言,TypeScript会通过类型守卫的返回值自动完成精确的类型推导,是更推荐的做法。
方案3:借助条件类型别名简化代码
如果觉得Extract<T, { tag: "Foo" }>写起来繁琐,可以定义一个条件类型别名:
type MyUnion = | { tag: "Foo"; field: string; } | { tag: "Bar"; } | null; type FooFrom<T extends MyUnion> = Extract<T, { tag: "Foo" }>; function foo<T extends MyUnion>(value: T) { if (value !== null && value.tag === "Foo") { const fooValue: FooFrom<T> = value as FooFrom<T>; const callback: (v: FooFrom<T>) => void = (v) => console.log(v.field); callback(value as FooFrom<T>); } }
这只是语法糖,核心还是依赖类型断言,适合需要多次使用该类型的场景。
总结
泛型上下文里,TypeScript对条件类型(如Extract)的推导存在局限性,无法自动关联类型收窄后的具体值与条件类型结果。通过自定义类型守卫可以让TypeScript获得明确的类型关联,而类型断言则是快速解决问题的折中方案,两种方式都能解决你遇到的赋值和回调调用错误。
内容的提问来源于stack exchange,提问作者Lukas Kalbertodt

