Automatic types for jest.fn() for mocked class methods

Viewed 1836

My project is strictly typed, with many limitations. Let's say I have some class with two methods.

class SomeClass {
  public someMethod(param1: string, param2: number): string {
    return `${param1}: ${param2}`;
  }
  public otherMethod(param3: string[]): boolean {
    return !!param3.length;
  }
}

I want to mock both functions, what normally I do: const mock = jest.fn() unfortuantely, strict types + eslint rules complain in many places for such approach. Few examples:

const mock = jest.fn();
mock.mockReturnValue(34); // no return value error
mock.mock.calls[0][0]; // ESLint: Unsafe member access [0] on an any value.(@typescript-eslint/no-unsafe-member-access)

I can provide types explictily:

const mock = jest.fn<string, [string, number]>();
mock.mockReturnValue(34); // TS2345: Argument of type '34' is not assignable to parameter of type 'string'
mock.mock.calls[0][0]; // ok

It is working but quite annoying (especially if we change function signatures, etc)

To avoid that I can make:

const mock1 = jest.fn<ReturnType<SomeClass['someMethod']>, Parameters<SomeClass['someMethod']>>();
const mock2 = jest.fn<ReturnType<SomeClass['otherMethod']>, Parameters<SomeClass['otherMethod']>>();

mock1.mockReturnValue(34); // error, TS2345: Argument of type '34' is not assignable to parameter of type 'string'
mock1.mockReturnValue('34'); // ok
mock2.mock.calls[0][1]; // error, TS2493: Tuple type '[string[]]' of length '1' has no element at index '1'.
mock2.mock.calls[0][0]; // ok

This looks nicer, but I need to repeat this ReturnType<SomeClass['someMethod']>, Parameters<SomeClass['someMethod']> a lot, would be good to have some automatic function for that.

That is where I fail, I tried for example reproduce it with following:

const typedJestFn = <T, K extends keyof T>() => jest.fn<ReturnType<T[K]>, Parameters<T[K]>>();
const mock4 = typedJestFn<SomeClass, 'someMethod'>();

But it gets:

TS2344: Type 'T[K]' does not satisfy the constraint '(...args: any) => any'.
   Type 'T[keyof T]' is not assignable to type '(...args: any) => any'.
     Type 'T[string] | T[number] | T[symbol]' is not assignable to type '(...args: any) => any'.
       Type 'T[string]' is not assignable to type '(...args: any) => any'.

Strangely enough the mock4 itself works as expected

PS:

    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
1 Answers

The issue in the question is that first we make a general type T and then take a key K from that T. Using T[K] we try to access that member which could be of any type (not restricted by template types).

So, while it is hard to filter functions (methods) from the class, it turned out we do not need that at all. We just need any function and T can be just a function.

Here is solution that I ended up with:

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const typedJestFn = <T extends (...args: any) => any>() => jest.fn<ReturnType<T>, Parameters<T>>();

Not so good that there is any, but it does not affect in any manner, it just informs that T should be of function type.

Related