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
相关产品推荐
相关产品推荐

