Search code examples
javascripttypescriptjestjsfetch-mock

Typescript complaining after passing ...args as argument to a function


I'm using fetch-mock library to build my own utils.

When defining a new function and passing args to fetchMock.mock function, I get this error:

A spread argument must either have a tuple type or be passed to a rest parameter

export function mockRequest(...args: any) {
  return fetchMock.mock(...args);
}

I could strictly define the amount of arguments, but would want to avoid it. Any suggestions how to keep ...args and make TypeScript happy?

I've tried various solutions such as using .apply and doing some casts as well but to no avail.


Solution

  • Problem

    fetchMock.mock is typed to accept 0-3 arguments:

    mock(matcher: MockMatcher | MockOptions, response: MockResponse | MockResponseFunction, options?: MockOptions): this;
    mock(options: MockOptions): this;
    mock(): this;
    

    wheresrhys/fetch-mock/types/index.d.ts:L270-L294

    An attempt is made to spread an array of any into it. any will not stop TypeScript from checking the potential length of the spread – it will only not check the type of the items in the tuple are acceptable for the function parameters. An example:

    function fn(a: number, b: number) {}
    
    const foo: any[] = [];
    const bar: [any, any] = ["one", "two"];
    
    fn(...foo); // Not OK ❌
    // ~~~~~~
    // A spread argument must either have a tuple type or be passed to a rest parameter.
    
    fn(...bar); // OK, even though we're passing in strings ✅
    

    TypeScript Playground

    Solution

    The error message tells you the solution (emphasis mine):

    A spread argument must either have a tuple type or be passed to a rest parameter.

    Let's explore both of them.

    Passing to a rest parameter

    If the function is typed to accept a rest parameter it works because rest parameters are essentially an infinite list of optional parameters. However, in your case, you don't have control of the interface of the library. Consequently, you'll have to use the other option.

    Type the argument as a tuple

    Tuples are a list that have two notable properties. They are:

    • Finite. This guarantees the length will match the length of the arguments to the function you're attempting to use.
    • Ordered. This guarantees the type of a value at each index will match with the function's argument at the same index.

    Using Parameters?

    tl;dr this doesn't work, see the linked GitHub issues

    TypeScript provides a utility function named Parameters which constructs a tuple type from the types used in the parameters of the function passed to it, so in theory you could do this:

    function mockRequest(...args: Parameters<typeof fetchMock.mock>) {
      return fetchMock.mock(...args);
    }
    

    TypeScript Playground

    However at the time of writing, Parameters only returns the parameters from the last overload (see TypeScript issue 14107 and TypeScript issue 32164). In the above example, mockRequest would be typed to accept no parameters because the last overload of fetchMock.mock has no parameters.

    Manual types

    As lovely as it would've been to use Parameters. It looks like we'll have to manually add types. Here's a working example:

    type Overload1 = [matcherOrOptions:  MockMatcher | MockOptions, response: MockResponse | MockResponseFunction, options?: MockOptions];
    type Overload2 = [matcherOrOptions: MockOptions];
    type Overload3 = [];
    
    type Args = Overload1 | Overload2 | Overload3;
    
    function mockRequest(...args: Args) {
      return args.length === 1 ?
        fetchMock.mock(...args) :
        args.length >= 2 ?
          fetchMock.mock(...args as Overload1) :
          fetchMock.mock();
    }
    

    TypeScript Playground

    For an underdetermined reason, TypeScript isn't able to narrow the type of args to Overload1 with the condition args.length >= 2 so a cast is necessary.