Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function could be instantiated with a different subtype of constraint

Tags:

typescript

TypeScript refuses to compile the debounce function because something wrong with type of the wrapping function:

export function debounce<F extends ((...args: any[]) => void)>(fn: F, timeout: number): F {
  let timer: NodeJS.Timeout | undefined

  // Problem here, TypeScript complains it's not the same function as F
  return ((...args: any[]) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => fn(...args), timeout)
  })
}

The error:

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

How to fix that? Without forced typecasting return ... as F or return ... as any

like image 381
Alex Craft Avatar asked Dec 02 '19 23:12

Alex Craft


1 Answers

The problem is that the constraint (...args: any[]) => void on F can be satisified by a number of types that you might find surprising, and the function you're returning will not be assignable to such types. For example:

debounce(() => "oopsie", 1000)().toUpperCase(); // okay at compile time, typeError at runtime

Here, the function type F returns a string value; this is assignable to a void-returning function type, as explained in the FAQ. But of course debounce() will not return a string-returning function, so the return type of debounce() is not the same F as passed in.

Also:

function foo() { };
foo.prop = 123; // property declaration
debounce(foo, 1000).prop.toFixed(); // okay at compile time, TypeError at runtime

In this case, we have a function with a property declared on it. So the F type here will be a function type ()=>void with an extra prop property. But again, debounce() will not return a function with this extra prop property on it, so the return type of debounce() is again not the same F as passed in.


The fix here is to make debounce() only generic enough to represent what you're actually doing. The returned function will take the same argument list as the passed-in function, so we need the argument list to be generic. And the returned function will definitely return void and is not going to have extra properties on it. So only the argument list needs a type parameter (say, A), and both the input and output functions will be of the type (...args: A) => void:

export function debounce<A extends any[]>(
    fn: (...args: A) => void,
    timeout: number
): (...args: A) => void {
    let timer: NodeJS.Timeout | undefined

    return ((...args: A) => {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => fn(...args), timeout)
    })
}

That compiles with no error. Okay, hope that helps; good luck!

Link to code

like image 164
jcalz Avatar answered Sep 27 '22 23:09

jcalz