Kevin
Kevin

Reputation: 303

Generic Function Subtype Constraint Error and Confusion

I was playing with the idea of a simple wrapper function using TypeScript. I ended up getting something working with the following:

export function logFn<T extends (...args: any[]) => any>(
  fn: T,
): (...args: Parameters<T>) => ReturnType<T>  {
  const log = (...args: Parameters<T>): ReturnType<T> => {
    console.log('log')
    return fn(...args)
  }
  return log
}

This works and the compiler is happy. My question is around my initial attempt which looked more like this

export function logFn<T extends (...args: any[]) => any>(
  fn: T,
): T  {
  const log: T = (...args) => {
    console.log('log')
    return fn(...args)
  }
  return log
}

This was giving me an error at the log variable declaration with the error:

Type '(...args: any[]) => any' is not assignable to type 'T'.
  '(...args: any[]) => any' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '(...args: any[]) => any'.

It seems that there's some subtype relationship constraint that I can't fully wrap my head around (this error isn't doing me too many favors either). Was hoping someone with a better mental grasp on this stuff could give me a decent explanation so I can be less confused by this behavior (which I'm sure is correct).

Upvotes: 4

Views: 499

Answers (1)

ed&#39;
ed&#39;

Reputation: 1905

The "problem" is this: T extends (...args: any[]) => any

Its more visualisable if you see the below version:

export function logFn<T extends (...args: any[]) => any>(fn: T): T {
  return (a, b) => fn(a, b);
}

You will see the error holds

Type '(a: any, b: any) => any' is not assignable to type 'T'. '(a: any, b: any) => any' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '(...args: any[]) => any'.

Both of the below are valid as far as T extends (...args: any[]) => any goes

  1. logFn((a, b) => a + b)
  2. logFn((a, b, c) => c)

But if you refer back to the example I gave, the inner definition as:

return (a, b) => fn(a, b);

So option 2. will throw an error here, which is why typescript is warning you about it.

logFn<T extends (...args: any[]) => any>(fn: T): T

We are going to receive a type T and return a type T. return (a, b) => fn(a, b); is a valid return type, it does extend (...args: any[]) => any but how can you be sure that the value passed to fn (T) matches that signature? (i.e. it could be another incompatible subtype of (...args: any[]) => any)

Not sure if I explained well enough, but thats my understanding

The reason your workaround is fine is because by adding (...args: Parameters<T>) => ReturnType<T> you are telling the compiler the parameters & return types must match those of the passed function, as opposed to T which could be any other function definition

Upvotes: 4

Related Questions