Discriminated Unions与类型转换:TypeScript中changeApiVersion函数为何无法缩小返回类型?
changeApiVersion Correctly? The core issue here is that TypeScript's type system can't automatically correlate the combination of two independent union-type parameters to infer a precise return type. Let's break this down and fix it.
Why Your Current Code Fails
Inside each branch of your switch/if statements, TypeScript can narrow the type of individual parameters (e.g., when targetVersion is '4', it knows to check if apiBodyInput is InputB and convert it to InputA). However, the function's overall return type is just the union of all possible branch return types (InputA | InputB), because TypeScript can't track the dependency between the apiBodyInput type and targetVersion as a whole. When you call the function, it can't map your specific input parameter combination to the exact return type—it only sees the union.
Solutions to Fix the Return Type Narrowing
1. Use Function Overloads
Function overloads let you explicitly define the relationship between specific input parameter combinations and their corresponding return types. Here's how to adapt your code:
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}`); } // Overload signatures: define precise input-output pairs export function changeApiVersion(apiBodyInput: InputB, targetVersion: '4'): InputA; export function changeApiVersion(apiBodyInput: InputA, targetVersion: '3'): InputB; export function changeApiVersion(apiBodyInput: InputA | InputB, targetVersion: '3' | '4'): InputA | InputB; // Implementation signature (hidden from callers) export function 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); } } // Now this works without errors! const myFoo = changeApiVersion({version: '3', bar: 'sad clown noise'}, 4).foo;
When you call the function with a specific input combination, TypeScript will match it to the corresponding overload signature and infer the correct return type.
2. Use Generic Conditional Types
You can also use generics with conditional types to explicitly tie the input parameters to the return type:
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 function changeApiVersion< T extends InputA | InputB, V extends '3' | '4' >( apiBodyInput: T, targetVersion: V ): T extends InputA ? V extends '3' ? InputB : InputA : V extends '4' ? InputA : InputB { if(apiBodyInput.version !== '3' && apiBodyInput.version !== '4'){ return assertNever(apiBodyInput) as never; } switch(targetVersion) { case '4': return (apiBodyInput.version === '3' ? convert3to4(apiBodyInput as InputB) : apiBodyInput) as never; case '3': return (apiBodyInput.version === '4' ? convert4to3(apiBodyInput as InputA) : apiBodyInput) as never; default: return assertNever(targetVersion) as never; } } // This also works correctly const myFoo = changeApiVersion({version: '3', bar: 'sad clown noise'}, 4).foo;
The conditional type checks the type of T (the input body) and V (the target version) to compute the exact return type. We use as never in the implementation to satisfy TypeScript's type checker, since it can't fully infer the conditional type internally.
内容的提问来源于stack exchange,提问作者AncientSwordRage

