TypeScript区分联合类型转换后返回类型无法收窄的原因
我写了一个用于转换API输入类型的TypeScript函数,用到了区分联合类型(Discriminated Unions)。switch语句的每个分支里,TypeScript能识别对应分支的返回类型,但调用函数时(比如传入明确参数获取foo属性),TypeScript却提示返回类型是InputA | InputB,报错foo属性不存在于InputB。就算调换判断逻辑结构,问题还是存在,为什么changeApiVersion函数没法在类型转换后收窄返回类型?
原代码示例
interface BaseInput { version: '3' | '4'; } interface InputA extends BaseInput{ version: '4'; foo: string; } interface InputB extends BaseInput{ version: '3'; bar: string; } const convert3to4 = (apiBodyInput: InputB) => { return { version: '4', foo: apiBodyInput.bar } as InputA; }; const convert4to3 = (apiBodyInput: InputA) => { return { version: '3', bar: apiBodyInput.foo } as InputB; }; function assertNever(version: never): never { throw new Error(`Unrecognised api version: ${version}`); } export const changeApiVersion = (apiBodyInput: InputA | InputB, targetVersion: '3' | '4') => { if(apiBodyInput.version !== '3' && apiBodyInput.version !== '4'){ return assertNever(apiBodyInput) } switch(targetVersion) { case '4': return apiBodyInput.version === '3' ? convert3to4(apiBodyInput) : apiBodyInput; case '3': return apiBodyInput.version === '4' ? convert4to3(apiBodyInput) : apiBodyInput; default: return assertNever(targetVersion); } }
调换逻辑后的代码
export const changeApiVersion = (apiBodyInput: InputA | InputB, targetVersion: '3' | '4') => { if(targetVersion !== '3' && targetVersion !== '4'){ return assertNever(targetVersion) } switch(apiBodyInput.version) { case '3': return targetVersion === '4' ? convert3to4(apiBodyInput) : apiBodyInput; case '4': return targetVersion === '3' ? convert4to3(apiBodyInput) : apiBodyInput; default: return assertNever(apiBodyInput); } }
核心原因
TypeScript的类型系统无法自动关联两个独立联合类型参数(apiBodyInput的version和targetVersion)之间的逻辑关系。函数的返回类型是基于所有参数组合情况推导的,但TypeScript没法追踪这种跨参数的条件依赖,只能把所有可能的返回结果合并成联合类型,无法在调用时根据具体传入的参数组合精准收窄类型。
举个例子:当你传入InputA和targetVersion: '3',TypeScript在分支里知道会返回InputB,但函数整体的返回类型还是会被推导为InputA | InputB——因为它要覆盖所有参数组合的可能性,没法动态根据调用时的参数调整返回类型。
解决方案1:泛型+条件类型绑定类型关系
通过泛型绑定输入参数的具体类型,再用条件类型明确返回类型与输入参数的映射关系,让TypeScript能精准推导返回类型:
interface BaseInput { version: '3' | '4'; } interface InputA extends BaseInput{ version: '4'; foo: string; } interface InputB extends BaseInput{ version: '3'; bar: string; } const convert3to4 = (apiBodyInput: InputB): InputA => { return { version: '4', foo: apiBodyInput.bar }; }; const convert4to3 = (apiBodyInput: InputA): InputB => { return { version: '3', bar: apiBodyInput.foo }; }; function assertNever(value: never): never { throw new Error(`Unrecognised value: ${value}`); } // 定义条件类型,明确目标版本对应的返回类型 type TargetVersionMap<V extends '3' | '4'> = V extends '4' ? InputA : InputB; // 泛型函数,绑定输入参数类型与返回类型的关系 export function changeApiVersion< T extends InputA | InputB, V extends '3' | '4' >(apiBodyInput: T, targetVersion: V): TargetVersionMap<V> { switch (targetVersion) { case '4': return (apiBodyInput.version === '3' ? convert3to4(apiBodyInput as InputB) : apiBodyInput) as TargetVersionMap<V>; case '3': return (apiBodyInput.version === '4' ? convert4to3(apiBodyInput as InputA) : apiBodyInput) as TargetVersionMap<V>; default: return assertNever(targetVersion); } } // 调用示例:TypeScript可精准推导返回类型 const inputA: InputA = { version: '4', foo: 'test' }; const result3 = changeApiVersion(inputA, '3'); // 类型为InputB,可访问bar属性 const result4 = changeApiVersion(inputA, '4'); // 类型为InputA,可访问foo属性 const inputB: InputB = { version: '3', bar: 'test' }; const resultB3 = changeApiVersion(inputB, '3'); // 类型为InputB const resultB4 = changeApiVersion(inputB, '4'); // 类型为InputA
关键改动说明
- 泛型追踪参数类型:用
T绑定apiBodyInput的具体类型,V绑定targetVersion的具体类型,让TypeScript能识别调用时的参数组合。 - 条件类型定义映射:
TargetVersionMap明确了当目标版本是4时返回InputA,是3时返回InputB,建立输入与输出的强类型关联。 - 类型断言辅助推导:在switch分支中用类型断言告诉TypeScript当前返回值符合条件类型的结果(因为TypeScript仍无法自动跨参数推导这种复杂关系)。
解决方案2:函数重载(适合参数组合较少的场景)
为每一种参数组合定义对应的重载签名,TypeScript会根据调用时的参数匹配对应的签名,返回精准类型:
// 先定义函数重载签名,覆盖所有参数组合 export function changeApiVersion(apiBodyInput: InputA, targetVersion: '3'): InputB; export function changeApiVersion(apiBodyInput: InputA, targetVersion: '4'): InputA; export function changeApiVersion(apiBodyInput: InputB, targetVersion: '3'): InputB; export function changeApiVersion(apiBodyInput: InputB, targetVersion: '4'): InputA; // 函数实现部分 export function changeApiVersion(apiBodyInput: InputA | InputB, targetVersion: '3' | '4') { switch (targetVersion) { case '4': return apiBodyInput.version === '3' ? convert3to4(apiBodyInput as InputB) : apiBodyInput; case '3': return apiBodyInput.version === '4' ? convert4to3(apiBodyInput as InputA) : apiBodyInput; default: return assertNever(targetVersion); } }
这种方式更直观,不需要复杂的泛型和条件类型,适合参数组合有限的场景。
内容的提问来源于stack exchange,提问作者AncientSwordRage

