Skip to content

Commit 34acf3e

Browse files
committed
feat: Add type inference to parameters of 'have been called with' functions (jestjs#15034)
WIP will be amended
1 parent c54bccd commit 34acf3e

File tree

2 files changed

+243
-3
lines changed

2 files changed

+243
-3
lines changed

packages/expect/src/types.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import type {EqualsFunction, Tester} from '@jest/expect-utils';
1010
import type * as jestMatcherUtils from 'jest-matcher-utils';
11+
// TODO does this require dependency in package.json?
12+
import type {Mock} from 'jest-mock';
1113
import type {INTERNAL_MATCHER_FLAG} from './jestMatchersObject';
1214

1315
export type SyncExpectationResult = {
@@ -231,16 +233,16 @@ export interface Matchers<R extends void | Promise<void>, T = unknown> {
231233
/**
232234
* Ensure that a mock function is called with specific arguments.
233235
*/
234-
toHaveBeenCalledWith(...expected: Array<unknown>): R;
236+
toHaveBeenCalledWith(...expected: FunctionParameters<T>): R;
235237
/**
236238
* Ensure that a mock function is called with specific arguments on an Nth call.
237239
*/
238-
toHaveBeenNthCalledWith(nth: number, ...expected: Array<unknown>): R;
240+
toHaveBeenNthCalledWith(nth: number, ...expected: FunctionParameters<T>): R;
239241
/**
240242
* If you have a mock function, you can use `.toHaveBeenLastCalledWith`
241243
* to test what arguments it was last called with.
242244
*/
243-
toHaveBeenLastCalledWith(...expected: Array<unknown>): R;
245+
toHaveBeenLastCalledWith(...expected: FunctionParameters<T>): R;
244246
/**
245247
* Use to test the specific value that a mock function last returned.
246248
* If the last call to the mock function threw an error, then this matcher will fail
@@ -307,3 +309,99 @@ export interface Matchers<R extends void | Promise<void>, T = unknown> {
307309
*/
308310
toThrow(expected?: unknown): R;
309311
}
312+
313+
// TODO add more overload options (up to 10?)
314+
type FunctionParameters<M> =
315+
M extends Mock<infer F>
316+
? F extends OverloadedFunction4
317+
? FunctionParameters4<F>
318+
: F extends OverloadedFunction3
319+
? FunctionParameters3<F>
320+
: F extends OverloadedFunction2
321+
? FunctionParameters2<F>
322+
: F extends NoOverloadsFunction
323+
? FunctionParameters1<F>
324+
: Array<unknown>
325+
: Array<unknown>;
326+
327+
type NoOverloadsFunction = (...args: any) => any;
328+
329+
type OverloadedFunction2 = {
330+
(...args: any): any;
331+
(...args: any): any;
332+
};
333+
type OverloadedFunction3 = {
334+
(...args: any): any;
335+
(...args: any): any;
336+
(...args: any): any;
337+
};
338+
type OverloadedFunction4 = {
339+
(...args: any): any;
340+
(...args: any): any;
341+
(...args: any): any;
342+
(...args: any): any;
343+
};
344+
345+
type WithAsymmetricMatchers<P extends Array<any>> = {
346+
[K in keyof P]: P[K] | AsymmetricMatcher;
347+
};
348+
349+
type FunctionParameters1<F extends NoOverloadsFunction> = F extends (
350+
...args: infer P
351+
) => any
352+
? WithAsymmetricMatchers<P>
353+
: never;
354+
355+
type FunctionParameters2<F extends OverloadedFunction2> = F extends {
356+
(...args: infer P1): any;
357+
(...args: infer P2): any;
358+
}
359+
? WithAsymmetricMatchers<P1> | WithAsymmetricMatchers<P2>
360+
: never;
361+
362+
type FunctionParameters3<F extends OverloadedFunction2> = F extends {
363+
(...args: infer P1): any;
364+
(...args: infer P2): any;
365+
(...args: infer P3): any;
366+
}
367+
?
368+
| WithAsymmetricMatchers<P1>
369+
| WithAsymmetricMatchers<P2>
370+
| WithAsymmetricMatchers<P3>
371+
: never;
372+
373+
type FunctionParameters4<F extends OverloadedFunction2> = F extends {
374+
(...args: infer P1): any;
375+
(...args: infer P2): any;
376+
(...args: infer P3): any;
377+
(...args: infer P4): any;
378+
}
379+
?
380+
| WithAsymmetricMatchers<P1>
381+
| WithAsymmetricMatchers<P2>
382+
| WithAsymmetricMatchers<P3>
383+
| WithAsymmetricMatchers<P4>
384+
: never;
385+
386+
// TODO delete
387+
// const ama: AsymmetricMatchers = null;
388+
// const x: Mock<(s: string, n: number) => void> = null;
389+
// const ex: Expect = null;
390+
// const y = ex(x);
391+
// y.toHaveBeenCalledWith('s', 1);
392+
// y.toHaveBeenCalledWith(ama.stringContaining('sd'), 1);
393+
// y.toHaveBeenCalledWith();
394+
395+
// function withOverload(): void;
396+
// function withOverload(n: number): void;
397+
// function withOverload(n: number, s: string): void;
398+
// function withOverload(n?: number, s?: string): void {}
399+
//
400+
// const pp: FunctionParameters<Mock<typeof withOverload>> = null;
401+
//
402+
// const wo: typeof withOverload = null;
403+
// const x: Mock<typeof withOverload> = null;
404+
// const ex: Expect = null;
405+
// const y = ex(x);
406+
// y.toHaveBeenCalledWith();
407+
// y.toHaveBeenCalledWith(1, 's');

packages/jest-types/__typetests__/expect.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,147 @@ expect(
307307
),
308308
).type.toBeVoid();
309309

310+
expect(
311+
jestExpect(jest.fn<() => void>()).toHaveBeenCalledWith(),
312+
).type.toBeVoid();
313+
expect(
314+
jestExpect(jest.fn<() => void>()).toHaveBeenCalledWith(1),
315+
).type.toRaiseError();
316+
317+
expect(
318+
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith(),
319+
).type.toBeVoid();
320+
expect(
321+
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith(123),
322+
).type.toBeVoid();
323+
expect(
324+
jestExpect(jest.fn<(n?: number) => void>()).toHaveBeenCalledWith('value'),
325+
).type.toRaiseError();
326+
327+
expect(
328+
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith(123),
329+
).type.toBeVoid();
330+
expect(
331+
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith(),
332+
).type.toRaiseError();
333+
expect(
334+
jestExpect(jest.fn<(n: number) => void>()).toHaveBeenCalledWith('value'),
335+
).type.toRaiseError();
336+
337+
expect(
338+
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith('value'),
339+
).type.toBeVoid();
340+
expect(
341+
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith(),
342+
).type.toRaiseError();
343+
expect(
344+
jestExpect(jest.fn<(s: string) => void>()).toHaveBeenCalledWith(123),
345+
).type.toRaiseError();
346+
347+
expect(
348+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
349+
123,
350+
'value',
351+
),
352+
).type.toBeVoid();
353+
expect(
354+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(),
355+
).type.toRaiseError();
356+
expect(
357+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
358+
123,
359+
),
360+
).type.toRaiseError();
361+
expect(
362+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
363+
123,
364+
123,
365+
),
366+
).type.toRaiseError();
367+
expect(
368+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
369+
'value',
370+
'value',
371+
),
372+
).type.toRaiseError();
373+
expect(
374+
jestExpect(jest.fn<(n: number, s: string) => void>()).toHaveBeenCalledWith(
375+
'value',
376+
123,
377+
),
378+
).type.toRaiseError();
379+
380+
expect(
381+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
382+
123,
383+
'value',
384+
),
385+
).type.toBeVoid();
386+
expect(
387+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
388+
123,
389+
),
390+
).type.toBeVoid();
391+
expect(
392+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(),
393+
).type.toRaiseError();
394+
expect(
395+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
396+
'value',
397+
),
398+
).type.toRaiseError();
399+
expect(
400+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
401+
'value',
402+
'value',
403+
),
404+
).type.toRaiseError();
405+
expect(
406+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
407+
'value',
408+
123,
409+
),
410+
).type.toRaiseError();
411+
expect(
412+
jestExpect(jest.fn<(n: number, s?: string) => void>()).toHaveBeenCalledWith(
413+
123,
414+
123,
415+
),
416+
).type.toRaiseError();
417+
418+
// TODO test overloaded with union type?
419+
function overloaded(): void;
420+
// eslint-disable-next-line @typescript-eslint/unified-signatures
421+
function overloaded(n: number): void;
422+
// eslint-disable-next-line @typescript-eslint/unified-signatures
423+
function overloaded(n: number, s: string): void;
424+
function overloaded(n?: number, s?: string): void {
425+
// noop
426+
}
427+
428+
expect(
429+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(),
430+
).type.toBeVoid();
431+
expect(
432+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123),
433+
).type.toBeVoid();
434+
expect(
435+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123, 'value'),
436+
).type.toBeVoid();
437+
expect(
438+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(123, 123),
439+
).type.toRaiseError();
440+
expect(
441+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith('value'),
442+
).type.toRaiseError();
443+
expect(
444+
jestExpect(jest.fn<typeof overloaded>()).toHaveBeenCalledWith(
445+
'value',
446+
'value',
447+
),
448+
).type.toRaiseError();
449+
450+
// TODO add typed parameters tests
310451
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith()).type.toBeVoid();
311452
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith('value')).type.toBeVoid();
312453
expect(jestExpect(jest.fn()).toHaveBeenLastCalledWith(123)).type.toBeVoid();
@@ -322,6 +463,7 @@ expect(
322463
).toHaveBeenLastCalledWith(jestExpect.stringContaining('value'), 123),
323464
).type.toBeVoid();
324465

466+
// TODO add typed parameters tests
325467
expect(jestExpect(jest.fn()).toHaveBeenNthCalledWith(2)).type.toBeVoid();
326468
expect(
327469
jestExpect(jest.fn()).toHaveBeenNthCalledWith(1, 'value'),

0 commit comments

Comments
 (0)