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

TypeScript类型安全输出构造函数实现难题求助

问题:实现类型安全的Input到Output转换函数

我定义了一组不同的Input类型:

interface StringInput {
  _tag: "_string";
}

interface NumberInput {
  _tag: "_number";
}

interface ListInput {
  _tag: "_list";
}

type Input = StringInput | NumberInput | ListInput;

以及对应的Output类:

class StringOutput {
  _tag = "StringOutput" as const;
  constructor(_: StringInput) {}
}

class NumberOutput {
  _tag = "NumberOutput" as const;
  constructor(_: NumberInput) {}
}

class ListOutput {
  _tag = "ListOutput" as const;
  constructor(_: ListInput) {}
}

type Output = StringOutput | NumberOutput | ListOutput;

需要实现一个函数,根据传入的Input生成对应的Output,且保证实现层面的类型安全性。

现有尝试的方案及问题

方案1:switch语句

利用switch的类型细化特性,但泛型未被细化导致返回类型不匹配:

namespace Attempt1 {

  type RemoveUn<S> = S extends `_${infer R}` ? R : S

  type InputOutputMap = {
    [K in Input["_tag"]]: Extract<Output,  { _tag: `${Capitalize<RemoveUn<K>>}Output` }>
  }

  function createValueOfObjKey<
    const I extends Input
  >(i: I): InputOutputMap[I["_tag"]] {
    switch(i._tag) {
      case "_string": {
        return new StringOutput(i); // 类型错误
      }
      case "_number": {
        return new NumberOutput(i); // 类型错误
      }
      case "_list": {
        return new ListOutput(i); // 类型错误
      }
    }
  }

}

问题:分支中i的类型虽被细化,但泛型I未同步细化,导致返回值类型无法与函数签名匹配。

方案2:索引访问类型

返回类型与签名一致,但丢失了i的类型细化,需要手动断言:

namespace Attempt2 {

  type RemoveUn<S> = S extends `_${infer R}` ? R : S

  type InputOutputMap = {
    [K in Input["_tag"]]: Extract<Output,  { _tag: `${Capitalize<RemoveUn<K>>}Output` }>
  }

  function createValueOfObjKey<
    const I extends Extract<Input, { _tag: K }>,
    const K extends Input["_tag"] = I["_tag"]
  >(i: I): InputOutputMap[K] {
    return {
      get _string() {
        return new StringOutput(i as StringInput);
      },
      get _number() {
        return new NumberOutput(i as NumberInput);
      },
      get _list() {
        return new ListOutput(i as ListInput);
      }
    }[i._tag];
  }

}

问题:无法利用类型细化,必须通过as断言绕过类型检查,存在安全隐患。

解决思路

思路1:函数重载

通过显式的函数重载签名,明确每个Input对应的Output类型,实现体中switch的类型细化可完美匹配:

// 重载签名:明确输入输出的对应关系
function createValue(i: StringInput): StringOutput;
function createValue(i: NumberInput): NumberOutput;
function createValue(i: ListInput): ListOutput;
// 实现签名:处理所有Input类型
function createValue(i: Input): Output {
  switch(i._tag) {
    case "_string":
      return new StringOutput(i);
    case "_number":
      return new NumberOutput(i);
    case "_list":
      return new ListOutput(i);
    default:
      throw new Error(`未知输入类型: ${i._tag}`);
  }
}

优点:类型安全无断言,调用时类型推断精准,实现逻辑直观。缺点:新增Input/Output类型时需同步添加重载签名,维护成本随类型数量增加而上升。

思路2:泛型映射+安全断言

优化映射类型,在分支中用安全断言匹配返回类型:

// 直接定义Input与Output的映射关系,避免字符串拼接带来的类型不确定性
type InputOutputMap = {
  "_string": StringOutput;
  "_number": NumberOutput;
  "_list": ListOutput;
};

function createValue<I extends Input>(i: I): InputOutputMap[I["_tag"]] {
  switch(i._tag) {
    case "_string":
      // 分支中i已被细化为StringInput,断言安全
      return new StringOutput(i) as InputOutputMap[I["_tag"]];
    case "_number":
      return new NumberOutput(i) as InputOutputMap[I["_tag"]];
    case "_list":
      return new ListOutput(i) as InputOutputMap[I["_tag"]];
    default:
      throw new Error(`未知输入类型: ${i._tag}`);
  }
}

优点:只需维护一处映射关系,泛型保证调用时的类型推断准确。缺点:每个分支需要添加断言,但断言是安全的(分支内类型已细化)。

思路3:构造函数映射+satisfies约束

将Input的_tag与对应的Output构造函数绑定,用satisfies确保映射正确性:

// 用satisfies约束映射对象,确保每个_tag对应正确的构造函数
const inputOutputConstructors = {
  "_string": StringOutput,
  "_number": NumberOutput,
  "_list": ListOutput,
} satisfies Record<Input["_tag"], new (i: Input) => Output>;

function createValue<I extends Input>(i: I): InstanceType<typeof inputOutputConstructors[I["_tag"]]> {
  // 构造函数已通过satisfies约束,断言安全
  return new inputOutputConstructors[i._tag](i) as InstanceType<typeof inputOutputConstructors[I["_tag"]]>;
}

优点:新增类型时只需修改映射对象,维护成本低;satisfies确保映射的类型正确性,避免手动拼接字符串的误差。缺点:需要TypeScript 5.0+版本支持satisfies关键字。

内容的提问来源于stack exchange,提问作者Andrea Simone Costa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 10:19:54