Skip to content

Commit c515dc0

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

File tree

2 files changed

+204
-3
lines changed

2 files changed

+204
-3
lines changed

packages/expect/src/types.ts

Lines changed: 62 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,60 @@ export interface Matchers<R extends void | Promise<void>, T = unknown> {
307309
*/
308310
toThrow(expected?: unknown): R;
309311
}
312+
313+
type FunctionParameters<M> =
314+
FunctionParametersInternal<M> extends never
315+
? Array<unknown>
316+
: FunctionParametersInternal<M>;
317+
318+
// TODO add more overload options (up to 10?)
319+
type FunctionParametersInternal<M> =
320+
M extends Mock<infer F>
321+
? F extends {
322+
(...args: infer P1): any;
323+
(...args: infer P2): any;
324+
(...args: infer P3): any;
325+
(...args: infer P4): any;
326+
}
327+
?
328+
| WithAsymmetricMatchers<P1>
329+
| WithAsymmetricMatchers<P2>
330+
| WithAsymmetricMatchers<P3>
331+
| WithAsymmetricMatchers<P4>
332+
: F extends (...args: infer P) => any
333+
? WithAsymmetricMatchers<P>
334+
: never
335+
: never;
336+
337+
type WithAsymmetricMatchers<P extends Array<any>> =
338+
Array<unknown> extends P ? never : {[K in keyof P]: P[K] | AsymmetricMatcher};
339+
340+
// type WithAsymmetricMatchers<P extends Array<any>> = {
341+
// [K in keyof P]: P[K] | AsymmetricMatcher;
342+
// };
343+
344+
// TODO delete
345+
// const ama: AsymmetricMatchers = null;
346+
// const x: Mock<(s: string, n: number) => void> = null;
347+
// const ex: Expect = null;
348+
// const y = ex(x);
349+
// y.toHaveBeenCalledWith('s', 1);
350+
// y.toHaveBeenCalledWith(ama.stringContaining('sd'), 1);
351+
// y.toHaveBeenCalledWith();
352+
//
353+
// function withOverload(): void;
354+
// function withOverload(n: number): void;
355+
// function withOverload(n: number, s: string): void;
356+
// function withOverload(n?: number, s?: string): void {}
357+
//
358+
// const pp1: FunctionParameters<Mock<() => void>> = null;
359+
// const pp2: FunctionParameters<Mock<(s: string) => void>> = null;
360+
// const pp3: FunctionParameters<Mock<typeof withOverload>> = null;
361+
// const pp4: FunctionParameters<Mock> = null;
362+
//
363+
// const wo: typeof withOverload = null;
364+
// const x: Mock<typeof withOverload> = null;
365+
// const ex: Expect = null;
366+
// const y = ex(x);
367+
// y.toHaveBeenCalledWith();
368+
// 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)