I have developed a helper mock function but I am facing issues with getting the types right. The current implementation is as follows:
import includes from 'ramda/src/includes';
function fn<T, Y extends any[]> (implementation: ((...args: Y) => T) = () => {}) {
const mockFn = (...args: Y) => {
mockFn.calls.push(args);
return implementation(...args);
};
mockFn.calls = [];
mockFn.hasBeenCalled = () => mockFn.calls.length > 0;
mockFn.hasBeenCalledWith = (...args: Y) => includes(args, mockFn.calls);
return mockFn;
}
Here is an example in the playground.
TypeScript raises two complaints.
Firstly, it objects to implementation
stating:
Type '() => void' is not assignable to type '(...args: Y) => T'.
Type 'void' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'void'.
Secondly, it points out an issue with mockFn.calls
:
Member 'calls' implicitly has an 'any[]' type.
The usage of the mocked function should be like this:
// with implementation
const originalFunction = (a: number, b: number) => a + b; // e.g. a simple add function
const mockedFn = fn(originalFunction);
mockedFn.hasBeenCalled();
// false
mockedFn(21, 21);
// 42
mockedFn.hasBeenCalled();
// true
mockedFn.hasBeenCalledWith(21);
// false
mockedFn.hasBeenCalledWith(21, 21);
// true
It should also work without implementation (defaulting to () => {}
).
const mockFn = fn();
// etc.
It would be great if TypeScript could recognize that mockedFn
has the same function signature as originalFunction
, while also exposing .calls
, hasBeenCalled
, and hasBeenCalledWith
.
In the current implementation, TypeScript seems aware of hasBeenCalled
and hasBeenCalledWith
, defining them as:
mockFn.hasBeenCalled(): boolean
mockFn.hasBeenCalledWith(...args: Y): boolean
How can I resolve these type errors so that TypeScript acknowledges the capabilities of fn
?