I need to type the return type of a typescript function based on the value of a parameter. The parameter provides the expected type of the returned value.
I have tried with generics:
function getValue<
ValueType extends 'STRING' | 'INTEGER',
R extends ValueType extends 'STRING'
? string | undefined
: ValueType extends 'INTEGER'
? number | undefined
: never
>(expectedType: ValueType): R {
if (expectedType === 'STRING') {
return "Hello"
}
if (expectedType === 'INTEGER') {
return 25
}
return undefined
}
But it fails on each return:
Type string is not assignable to type R string is assignable to the constraint of type R, but R could be instantiated with a different subtype of constraint string | number
I have tried with function overload:
function getValueWithOverload(expectedType: 'STRING'): string | undefined
function getValueWithOverload(expectedType: 'INTEGER'): number | undefined
function getValueWithOverload(
expectedType: 'STRING' | 'INTEGER'
): string | number | undefined {
if (expectedType === 'STRING') {
return "Hello"
}
if (expectedType === 'INTEGER') {
return 25
}
return undefined
}
Typescript accepts this last option, but also allows to do the following without any error:
if (expectedType === 'INTEGER') {
return "some string"
}
I have simplified the types, but there are around 6 possible types, not just 'STRING' and 'INTEGER'.
In TypeScript 5.7 and below there is no solution which is verified as type safe by the type checker. You'll have to loosen the type checking with something like type assertions (e.g., return "Hello" as any
) or with overloads (as you've seen, overload implementations are checked loosely against their call signatures; it is considered too complex to check each one accurately; see microsoft/TypeScript#13235).
The problem is that TypeScript can't doesn't use control flow analysis inside generic functions that return conditional types in order to validate them. This is the subject of the feature request microsoft/TypeScript#33912.
Luckily, that feature has been implemented in microsoft/TypeScript#56941 and merged into the main branch, so it should be released with TypeScript 5.8. At that point, you will be able to write a generic function that returns a conditional type of a particular form, and TypeScript will check it the way you expected. We'll have to refactor your code so that the return type isn't an "aliased" generic type parameter (there's no guarantee that R
is equal to the conditional type, so TypeScript would still be correct in rejecting it) and so that you don't return undefined
at the end (that code is unreachable and TS narrows T
all the way to never
there, and undefined
doesn't match never
):
function getValue<
ValueType extends 'STRING' | 'INTEGER',
>(expectedType: ValueType):
ValueType extends 'STRING' ? string | undefined :
ValueType extends 'INTEGER' ? number | undefined :
never {
if (expectedType === 'STRING') {
return "Hello" // okay
}
if (expectedType === 'INTEGER') {
return 25 // okay
}
throw new Error("impossible");
}
function getValueBad<
ValueType extends 'STRING' | 'INTEGER',
>(expectedType: ValueType):
ValueType extends 'STRING' ? string | undefined :
ValueType extends 'INTEGER' ? number | undefined :
never {
if (expectedType === 'STRING') {
return "Hello"
}
if (expectedType === 'INTEGER') {
return "some string"; // error!
//~~~~ <-- Type 'string' is not assignable to type 'number'.(2322)
}
throw new Error("impossible");
}
Looks good!