TypeScript中如何为可区分联合类型定义输入输出同类型函数
你遇到的问题其实是TypeScript处理可区分联合时的常见痛点——既要保留输入输出的类型关联,又要让类型守卫在函数实现中正常工作。下面我会给出几种优雅的解决方案,同时分析你之前尝试方案的问题所在。
方案一:条件类型 + 类型映射(轻量首选)
我们可以先通过类型映射把每个kind和对应的具体Shape类型绑定,再用条件类型让函数返回类型严格匹配输入类型:
// 定义具体的Shape接口 interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; } // 类型映射:将kind字符串映射到对应的Shape类型 type ShapeMap = { square: Square; rectangle: Rectangle; circle: Circle; }; // 重新定义Shape联合类型(可选,但结构更清晰) type Shape = ShapeMap[keyof ShapeMap]; // 实现double函数 function double<S extends Shape>(s: S): S extends { kind: infer K } ? ShapeMap[K] : never { switch (s.kind) { case "square": // 类型守卫会自动将s收窄为Square,访问s.size完全安全 return { ...s, size: s.size * 2 } as ShapeMap["square"]; case "rectangle": return { ...s, width: s.width * 2, height: s.height * 2 } as ShapeMap["rectangle"]; case "circle": return { ...s, radius: s.radius * 2 } as ShapeMap["circle"]; default: throw new Error(`未知形状类型: ${s.kind}`); } }
方案优势:
- 类型映射
ShapeMap明确了kind与具体类型的对应关系,避免重复定义。 - 返回类型通过条件类型+类型推断自动匹配输入类型:调用时
double(square)会被自动推断为返回Square,无需手动指定类型。 - switch语句中的类型守卫正常生效,不会出现属性不存在的报错。
方案二:广义代数数据类型(GADT,更严谨)
如果需要更严格的类型绑定,可以利用TypeScript对GADT的支持,通过泛型参数直接关联输入输出的类型:
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; } // 用泛型K定义Shape,K代表kind的具体类型 type Shape<K extends string = string> = K extends "square" ? Square : K extends "rectangle" ? Rectangle : K extends "circle" ? Circle : never; // 泛型K绑定输入和输出的类型一致性 function double<K extends "square" | "rectangle" | "circle">(s: Shape<K>): Shape<K> { switch (s.kind) { case "square": return { ...s, size: s.size * 2 } as Shape<K>; case "rectangle": return { ...s, width: s.width * 2, height: s.height * 2 } as Shape<K>; case "circle": return { ...s, radius: s.radius * 2 } as Shape<K>; default: throw new Error(`未知形状类型: ${s.kind}`); } }
方案优势:
- 泛型参数
K直接将输入的kind与返回类型绑定,类型系统会严格检查输入输出的一致性。 - 后续扩展Shape类型时,只需在
Shape的条件类型中添加新分支即可,扩展性更强。
你之前尝试的方案为什么无效?
我们逐一分析问题:
直接使用Shape作为输入输出类型
function double(s: Shape): Shape { /* ... */ }类型系统无法跟踪输入和输出的具体子类型,只知道两者都是Shape,但无法保证是同一个子类型,因此
doubleSquare会被推断为Shape而非Square。简单泛型约束
T extends Shapefunction double<T extends Shape>(s: T): T { /* ... */ }虽然返回类型是
T,但TypeScript不会在switch语句中自动收窄泛型T的类型(因为T可以是Shape的任意子集,比如Square | Rectangle),所以无法确定s.size一定存在,从而报错。函数重载
function double(s: Square): Square; function double(s: Rectangle): Rectangle; function double(s: Circle): Circle; function double(s: Shape): Shape { /* ... */ }重载能让调用时的类型推导正常工作,但存在两个问题:一是必须手动枚举所有Shape子类型,扩展性差;二是实现部分的类型检查宽松——如果在
case "square"中错误返回Circle,类型系统不会给出任何提示,因为实现只需返回Shape即可。
临时解决方案的不足
你目前使用的类型断言方案:
const doubleSquare: Square = double(square) as typeof square;
虽然能消除类型错误,但需要手动添加断言,不仅繁琐,而且类型系统无法保证断言的正确性——如果double函数实现出错,断言会掩盖潜在的类型问题。
内容的提问来源于stack exchange,提问作者Leandro Facchinetti

