Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TS cannot infer function return type on its own

Consider:

const fcts = {
  fctNumber: () => 1,
  fctBoolean: () => true,
}

as expected, a function that simply selects from fcts returns the correct type based on the value of T

const getFct = <T extends keyof typeof fcts>(name: T) => {
  return fcts[name]
}

getFct('fctNumber') // () => number
getFct('fctBoolean') // () => boolean

The same function but returning the result of calling the selected function is expected to return the return type of the selected function, but TS instead infers number | boolean

const getResult = <T extends keyof typeof fcts>(name: T) => {
  const fct = fcts[name]
  return fct()
}

getResult('fctNumber') // expected number, got number | boolean
getResult('fctBoolean') // expected boolean, got number | boolean

Changing the last line to add what seems like a redundant assertion works:

return fct() as ReturnType<typeof fct>

...but why? that doesn't seem to be adding any new information

like image 447
Hans Avatar asked Oct 20 '25 19:10

Hans


1 Answers

TypeScript cannot perform the kind of higher order analysis needed to maintain the correlation between the type of name (a generic type K) and the return type of fcts[name]. It doesn't "see" fcts as an object of zero-arg functions which returns a type correlated with the type of name. When evaluating the type (typeof fcts)[K] as a callable function, the compiler first widens it to its constraint, which is (typeof fcts)[keyof typeof fcts], and becomes the union type (() => number) | (() => boolean). And once that happens there's no hope, since TypeScript can't do much with correlated union types either. This is the subject of microsoft/TypeScript#30581.

If you don't want to just use a type assertion for expedience, you could try refactoring as recommended in microsoft/TypeScript#47109. What we need is for fcts to be of a mapped type like { [K in keyof FctsReturn]: () => FctsReturn[K] } for an appropriately-defined FctsReturn. Then fcts[name] would be a single function of type () => FctsReturn[K], and calling it would result in FctsReturn[K]. No union involved.

You can use your existing code to derive FctsReturn, such as by renaming fcts out of the way (to, say, _fcts), computing FctsReturn using the ReturnType<T> utility type, and then giving fcts the necessary type annotation:

const _fcts = {
    fctNumber: () => 1,
    fctBoolean: () => true,
}
type FctsReturn = { [K in keyof typeof _fcts]: ReturnType<typeof _fcts[K]> };
const fcts: { [K in keyof FctsReturn]: () => FctsReturn[K] } = _fcts;

This looks like a roundabout way of doing nothing; surely the type of fcts is equivalent either way. But since the compiler cannot automatically "see" how to distribute ReturnType<T> across the elements of fcts, you have to do it yourself. Now this works:

const getResult = <K extends keyof FctsReturn>(name: K) => {
    const fct = fcts[name]
    return fct()
}

const num = getResult('fctNumber');
//    ^? const num: number

const boo = getResult('fctBoolean');
//    ^? const boo: boolean

Playground link to code

like image 173
jcalz Avatar answered Oct 22 '25 11:10

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!