Michal Kurz
Michal Kurz

Reputation: 2095

TypeScript doesn't correctly infer a deterministic return type of a function

I have some arbitrary myCallback function

const myCallback = (param: number) => {
  // doSomething
};

And I'm trying go make another function that may or may not receive the param and returns myCallback either bound or unmodified:

const useMyCallback = (optionalParam?: number) => {
  return optionalParam === undefined
    ? myCallback
    : () => myCallback(optionalParam);
};

But regardless of whether I pass the param in or not, the return value of myCallback is typed as (param: number) => void

const myCallback = (param: number) => {
  // doSomething
};

const useMyCallback = (optionalParam?: number) => {
  return optionalParam === undefined
    ? myCallback
    : () => myCallback(optionalParam);
};

const unboundCallback = useMyCallback(); // typed (param: number) => void as expected
const boundCallback = useMyCallback(1);  // typed (param: number) => void, expected () => void

Is there some trick to achieve what I want without casting each useMyCallback call?

Upvotes: 2

Views: 174

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187034

There's a few things going on here.


First, lets imagine you want to call this type:

type Fn = ((arg: number) => void ) | (() => void)

In javascript you can always call a function with more arguments than is necessary. But typescript will not allow you to call a function with less arguments than is required. And this type might be a function that requires an argument. So that means the only type safe way to call this function, is to provide an argument.

This is why boundCallback requires an argument.


Second, Typescript doesn't actually execute your code, so you haven't done enough to let it know that leaving out the argument changes the return type. I think the best way to do that is with overloads.

// Overload signatures
function useMyCallback(): typeof myCallback
function useMyCallback(optionalParam?: number): () => void

// Implementation
function useMyCallback(optionalParam?: number) {
  return typeof optionalParam === 'undefined'
    ? myCallback
    : () => myCallback(optionalParam);
};

These overloads tell typescript that certain argument patterns have certain return types. Then your function has an implementation that accepts the superset of arguments, and returns the superset of return values.

Now typescript can infer that when you use different arguments, you get different return values.

This way you completely avoid the problem I mention a the top of this answer because the return type is never a union of two function types. It's just a single function type, that varies based on the arguments.

Now this works:

const unboundCallback = useMyCallback(); //-> (param: number) => void
unboundCallback(0)

const boundCallback = useMyCallback(1); //-> () => void
boundCallback() 

Playground

Upvotes: 2

Related Questions