You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的条件类型中添加新分支即可,扩展性更强。

你之前尝试的方案为什么无效?

我们逐一分析问题:

  1. 直接使用Shape作为输入输出类型

    function double(s: Shape): Shape { /* ... */ }
    

    类型系统无法跟踪输入和输出的具体子类型,只知道两者都是Shape,但无法保证是同一个子类型,因此doubleSquare会被推断为Shape而非Square。

  2. 简单泛型约束T extends Shape

    function double<T extends Shape>(s: T): T { /* ... */ }
    

    虽然返回类型是T,但TypeScript不会在switch语句中自动收窄泛型T的类型(因为T可以是Shape的任意子集,比如Square | Rectangle),所以无法确定s.size一定存在,从而报错。

  3. 函数重载

    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 06:27:26