Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jest] add type inference #31094

Closed
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion types/expect-puppeteer/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Definitions by: Josh Goldberg <https://github.com/JoshuaKGoldberg>
// Tanguy Krotoff <https://github.com/tkrotoff>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0

/// <reference types="jest" />

Expand Down
2 changes: 1 addition & 1 deletion types/jest-axe/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/nickcolley/jest-axe
// Definitions by: Josh Goldberg <https://github.com/JoshuaKGoldberg>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.4
// TypeScript Version: 3.0

/// <reference types="jest" />

Expand Down
2 changes: 1 addition & 1 deletion types/jest-image-snapshot/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/americanexpress/jest-image-snapshot#readme
// Definitions by: Janeene Beeforth <https://github.com/dawnmist>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />

Expand Down
2 changes: 1 addition & 1 deletion types/jest-in-case/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/thinkmill/jest-in-case#readme
// Definitions by: Geovani de Souza <https://github.com/geovanisouza92>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />
/// <reference types="node" />
Expand Down
8 changes: 4 additions & 4 deletions types/jest-in-case/jest-in-case-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ function subtract(minuend: number, subtrahend: number) {
}

beforeEach(() => {
jest.spyOn(global, 'describe').mockImplementation((title, fn) => fn());
jest.spyOn(global, 'test').mockImplementation((name, fn) => fn());
jest.spyOn(global, 'describe').mockImplementation((title, fn) => jest.fn());
jest.spyOn(global, 'test').mockImplementation((name, fn) => jest.fn());
global.test.skip = jest.fn((name, fn) => fn());
global.test.only = jest.fn((name, fn) => fn());
});
Expand Down Expand Up @@ -54,8 +54,8 @@ test('array', () => {
});

test('object', () => {
jest.spyOn(global, 'describe').mockImplementation((title, fn) => fn());
jest.spyOn(global, 'test').mockImplementation((name, fn) => fn());
jest.spyOn(global, 'describe').mockImplementation((title, fn) => jest.fn());
jest.spyOn(global, 'test').mockImplementation((name, fn) => jest.fn());

const title = 'add(augend, addend)';

Expand Down
2 changes: 1 addition & 1 deletion types/jest-json-schema/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/americanexpress/jest-json-schema#readme
// Definitions by: Igor Korolev <https://github.com/deadNightTiger>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />
import * as ajv from "ajv";
Expand Down
2 changes: 1 addition & 1 deletion types/jest-matchers/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/facebook/jest#readme
// Definitions by: Joscha Feth <https://github.com/joscha>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />
export = expect;
2 changes: 1 addition & 1 deletion types/jest-plugin-context/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/negativetwelve/jest-plugins/tree/master/packages/jest-plugin-context
// Definitions by: Jonas Heinrich <https://github.com/jonasheinrich>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />

Expand Down
2 changes: 1 addition & 1 deletion types/jest-specific-snapshot/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Project: https://github.com/igor-dv/jest-specific-snapshot#readme
// Definitions by: Janeene Beeforth <https://github.com/dawnmist>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
// TypeScript Version: 3.0

/// <reference types="jest" />

Expand Down
23 changes: 7 additions & 16 deletions types/jest-when/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,17 @@
// Project: https://github.com/timkindberg/jest-when#readme
// Definitions by: Alden Taylor <https://github.com/aldentaylor>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

/// <reference types="jest" />

export interface PartialMockInstance<T> {
mockReturnValue: jest.MockInstance<T>['mockReturnValue'];
mockReturnValueOnce: jest.MockInstance<T>['mockReturnValueOnce'];
mockResolvedValue: jest.MockInstance<T>['mockResolvedValue'];
mockResolvedValueOnce: jest.MockInstance<T>['mockResolvedValueOnce'];
mockRejectedValue: jest.MockInstance<T>['mockRejectedValue'];
mockRejectedValueOnce: jest.MockInstance<T>['mockRejectedValueOnce'];
}
export type PartialMockInstance<T, Y extends any[]> = Pick<jest.MockInstance<T, Y>, 'mockReturnValue' | 'mockReturnValueOnce' | 'mockResolvedValue'
| 'mockResolvedValueOnce' | 'mockRejectedValue' | 'mockRejectedValueOnce'>;

export interface When {
<T>(fn: jest.Mocked<T> | jest.Mock<T>): When;
// due to no-unnecessary-generics lint rule, the generics have been replaced with 'any'
// calledWith<T>(...matchers: any[]): PartialMockInstance<T>;
// expectCalledWith<T>(...matchers: any[]): PartialMockInstance<T>;
calledWith(...matchers: any[]): PartialMockInstance<any>;
expectCalledWith(...matchers: any[]): PartialMockInstance<any>;
export interface When<T = {}, Y extends any[] = any[]> {
(fn: jest.Mock<T, Y>): When<T, Y>;
calledWith(...matchers: Y): PartialMockInstance<T, Y>;
expectCalledWith(...matchers: Y): PartialMockInstance<T, Y>;
}

export const when: When;
16 changes: 10 additions & 6 deletions types/jest-when/jest-when-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@ describe('mock-when test', () => {

it('Supports compound declarations:', () => {
const fn = jest.fn();
when(fn).calledWith(1).mockReturnValue('no');
when(fn).calledWith(1).mockReturnValueOnce('no').mockReturnValue('yes');
when(fn).calledWith(2).mockReturnValue('way?');
when(fn).calledWith(3).mockReturnValue('yes');
when(fn).calledWith(4).mockReturnValue('way!');
when(fn).calledWith(3).mockResolvedValueOnce('no');
when(fn).calledWith(3).mockResolvedValue('yes');
when(fn).calledWith(4).mockRejectedValueOnce('no');
when(fn).calledWith(4).mockRejectedValue('yes');

expect(fn(1)).toEqual('no');
expect(fn(1)).toEqual('yes');
expect(fn(2)).toEqual('way?');
expect(fn(3)).toEqual('yes');
expect(fn(4)).toEqual('way!');
expect(fn(5)).toEqual(undefined);
expect(fn(3)).resolves.toEqual('no');
expect(fn(3)).resolves.toEqual('yes');
expect(fn(4)).rejects.toEqual('no');
expect(fn(4)).rejects.toEqual('yes');
});

it('Assert the args:', () => {
Expand Down
63 changes: 30 additions & 33 deletions types/jest/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
// Martin Hochel <https://github.com/hotell>
// Sebastian Sebald <https://github.com/sebald>
// Andy <https://github.com/andys8>
// Antoine Brault <https://github.com/antoinebrault>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 3.0

declare var beforeAll: jest.Lifecycle;
declare var beforeEach: jest.Lifecycle;
Expand All @@ -35,6 +36,8 @@ declare var xtest: jest.It;

declare const expect: jest.Expect;

type ArgsType<T> = T extends (...args: infer A) => any ? A : never;

interface NodeRequire {
/**
* Returns the actual module instead of a mock, bypassing all checks on
Expand Down Expand Up @@ -110,19 +113,15 @@ declare namespace jest {
/**
* Creates a mock function. Optionally takes a mock implementation.
*/
function fn<T extends {}>(implementation: (...args: any[]) => T): Mock<T>;
/**
* Creates a mock function. Optionally takes a mock implementation.
*/
function fn<T>(implementation?: (...args: any[]) => any): Mock<T>;
function fn<T, Y extends any[]>(implementation?: (...args: Y) => T): Mock<T, Y>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need Y extends never[], otherwise this refuses functions that have never parameters (rare, but possible).

declare function unreachable(arg: never): never

jest.fn(unreachable) // will error with `any[]`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, is there any reason why this is not just function fn<T extends (...args: never[]) => unknown>(implementation?: T): Mock<T>, with Mock changed to hold a function generic instead?

It makes declaring the Mock methods more annoying but it makes using .fn a lot easier when you want to make sure your mock function conforms to the interface of a function that already exists. Just call jest.fn<typeof myfunc>(/* inline function will even get contextual params/return types */).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the default type (when implementation is not specified) should be (...args: any[]) => void. Perhaps it should be on a 0 argument overload instead of an optional argument to avoid misuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need Y extends never[], otherwise this refuses functions that have never parameters (rare, but possible).

declare function unreachable(arg: never): never

jest.fn(unreachable) // will error with `any[]`

This is false. I just updated the tests and it doesn't error.

Actually, is there any reason why this is not just function fn<T extends (...args: never[]) => unknown>(implementation?: T): Mock<T>, with Mock changed to hold a function generic instead?

It makes declaring the Mock methods more annoying but it makes using .fn a lot easier when you want to make sure your mock function conforms to the interface of a function that already exists. Just call jest.fn<typeof myfunc>(/* inline function will even get contextual params/return types */).

Because this would be a breaking change. By adding a new generic with a default value, existing code will not be affected.

Also, the default type (when implementation is not specified) should be (...args: any[]) => void. Perhaps it should be on a 0 argument overload instead of an optional argument to avoid misuse.

It should not return void. This should compile:

jest.fn().mockImplementation((test: number) => test);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... I need to brush up on the ...args: never[] thing, then. I had seen it as a recommendation from the TS team somewhere a few months ago.

As for the second one, that's because you're specifying a mock implementation after the .fn call but... that is supported API because JS is weird like that 😵

If you use .fn() without also adding a more specific .mock* it'll return void by default. But currently methods cannot alter the type of anything on their this...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoinebrault Can you help me understand how to use the new:

function fn<T, Y extends any[]>(implementation: (...args: Y) => T): Mock<T, Y>;

I understand the T, but what is expected for the Y. After updating to this version, several of the mockImplementations, are throwing typing issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josecolella Y is the arguments types of the implementation function.

function x(y: number) {
  return y + '';
}
jest.fn(x) // will be Mock<string, [number]>

When mocking implementations, you have to use the same arguments types as the original function, but arguments are optional.

function x(y: number) {
  return y + '';
}
jest.fn(x).mockImplementation((y) => y * 2 + ''); // y is inferred to a number
jest.fn(x).mockImplementation(() => ''); // will work too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoinebrault What about for mockImplementation of classes? As of now it's complaining that I need to implement all the methods within the class, when in reality only one method needs to be mocked.

interface SomeInterface {
      foo() 
      foo2()
}

jest.fn<SomeInterface>(() => ({
 foo: jest.fn().mockReturnValue(true)
}));

With new types this no longer works. Do you have an example of how to use this new version with class mock implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoinebrault What about for mockImplementation of classes? As of now it's complaining that I need to implement all the methods within the class, when in reality only one method needs to be mocked.

you can use Partial for that use case. jest.Mocked<Partial<Type>>

interface SomeInterface {
      foo() 
      foo2()
}

jest.fn<SomeInterface>(() => ({
 foo: jest.fn().mockReturnValue(true)
}));

With new types this no longer works. Do you have an example of how to use this new version with class mock implementations?

interface SomeInterface {
    foo(): string,
    foo2(): number
}
const y: jest.Mocked<SomeInterface> = {
    foo: jest.fn().mockReturnValue(''),
    foo2: jest.fn().mockReturnValue(1)
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antoinebrault Thanks for your explanation and examples.

/**
* Use the automatic mocking system to generate a mocked version of the given module.
*/
function genMockFromModule<T>(moduleName: string): T;
/**
* Returns whether the given function is a mock function.
*/
function isMockFunction(fn: any): fn is Mock<any>;
function isMockFunction(fn: any): fn is Mock<any, any[]>;
/**
* Mocks a module with an auto-mocked version when it is being required.
*/
Expand Down Expand Up @@ -198,7 +197,9 @@ declare namespace jest {
* spy.mockRestore();
* });
*/
function spyOn<T extends {}, M extends keyof T>(object: T, method: M, accessType?: 'get' | 'set'): SpyInstance<T[M]>;
function spyOn<T extends {}, M extends keyof T>(object: T, method: M, accessType: 'get'): SpyInstance<T[M], []>;
function spyOn<T extends {}, M extends keyof T>(object: T, method: M, accessType: 'set'): SpyInstance<void, [T[M]]>;
antoinebrault marked this conversation as resolved.
Show resolved Hide resolved
function spyOn<T extends {}, M extends keyof T>(object: T, method: M): T[M] extends (...args: any[]) => any ? SpyInstance<ReturnType<T[M]>, ArgsType<T[M]>> : never;
antoinebrault marked this conversation as resolved.
Show resolved Hide resolved
/**
* Indicates that the module system should never return a mocked version of
* the specified module from require() (e.g. that it should always return the real module).
Expand Down Expand Up @@ -762,12 +763,12 @@ declare namespace jest {
new (...args: any[]): any;
}

interface Mock<T = {}> extends Function, MockInstance<T> {
new (...args: any[]): T;
(...args: any[]): any;
interface Mock<T, Y extends any[]> extends Function, MockInstance<T, Y> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please restore the default type parameter and add one for Y. I don't think making these required is worth the breaking change, and trying to "force people" to use stronger types in tests is very opinionated. A large percentage of jest.Mock usages don't touch values, only call counts. Also keep in mind the inference for stronger mock types will only help with spyOn. Every other declaration will need to have the parameters passed explicitly, which is going to be a bit painful since Y is an arguments tuple that needs to be extracted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that is it very opinionated, but shouldn't we, as a community, enforce best practices?

I strongly believe that testing should be done the safest way possible, and "any" type should be avoided at all cost.

I don't agree that stronger mock types will only help for spyOn. There is a couple of example I changed in jest-tests (mockContextVoid, mockContextString, spy3Mock, spy4) where the typing of arguments would be lost and potentially mislead people.

If you don't care about tuple generics, you can still use <any, any[]> or without typing :

const spy = spyOn(obj, 'method');
const mock = jest.fn();
const mockContextVoid = jest.fn().mock;
const mock2 = spy.mockImplementation(() => null);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please we want to move away from any when we are with typescript => so no default value with any

Arguments:

We need this contract for robust integrated tests (no any by default). I think this had to be corrected long ago (ts 2.8)
in other case the tests written are really weak.

This fix will put jasmine behind compared to jest and will raise the adoption of the library.

https://palantir.github.io/tslint/rules/no-any/

Using any as a type declaration nullifies the compile-time benefits of the type system.


Jest needs to guide devs to do it right and also produce quality tests. fallbacking on any is not at all going in this direction for the reasons posted on top.

If you plan to really go with this default value. I see one way to still force it by config:
It's to use the compiler flag noImplicitAny + the ts lint rule noAny.
This is a way to force it indirectly and keep default value any in jest types but i do not recommend this personnally.

@rickhanlonii can we have your opinion here, please?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A large percentage of jest.Mock usages don't touch values, only call counts

@jwbay can you explain this with an example of what you can do now and what this change would require?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that is it very opinionated, but shouldn't we, as a community, enforce best practices?

To a point. We need to balance the benefits and costs considering the pain of breaking changes. As a data point, it's possible to make a package so annoyingly type-safe the change is reverted: #26813

I strongly believe that testing should be done the safest way possible, and "any" type should be avoided at all cost.

You are absolutely entitled to that strong opinion in your projects. The issue here is that this change impacts a large number of developers across the world, from enterprise teams to personal projects (this package gets 2 million downloads per week), for value that people will question. This change as-is isn't really just opinionated—it's prescriptive.

I don't agree that stronger mock types will only help for spyOn. There is a couple of example I changed in jest-tests (mockContextVoid, mockContextString, spy3Mock, spy4) where the typing of arguments would be lost and potentially mislead people.

That's fair, I didn't qualify my statement correctly. spyOn is the only way to infer types from existing functions. Everything else will either need explicit types, a runtime function to extract them via inference, or some kind of conditional type to convert typeof MyFunction to a correctly populated jest.Mock. We should consider adding that last option with this change, actually, assuming it goes forward. Looks like it's just T extends (...args: infer R) => infer V ? jest.Mock<V, R> : never

If you don't care about tuple generics, you can still use <any, any[]> or without typing :

That's part of the problem with this. The <any, any[]> is often just noise. Reading noise sucks and writing noise really sucks. Here are a couple examples:

test('foo', () => {
  someFunction();
  // type params are just noise here
  (foo as jest.Mock<any, any[]>).mockClear();
  someFunction();
  expect(fooMock).toHaveBeenCalledTimes(1);
});

test('foo', () => {
  // more noise
  (foo as jest.Mock<any, any[]>).mockName('MyMock');
  const rendered = render(<Component foo={foo} />);
  expect(rendered).toMatchSnapshot();
});

I think providing defaults for the cases where the type parameters truly don't matter is a reasonable ask. How about a compromise of defaulting to never? That way you can't interact with arguments or return values unsafely by default, and you can't supply an invalid implementation. It would still be a massive breaking change, but at least the types involved are being touched in some way, so it's more reasonable as a value proposition.

I'd really like to get the thoughts of other maintainers here considering the impact: @NoHomey, @asvetliakov, @alexjoverm, @epicallan, @ikatyang, @wsmd, @JamieMason, @douglasduteil, @ahnpnl, @JoshuaKGoldberg, @UselessPickles, @r3nya, @Hotell, @sebald, @andys8

As a note for impact, things like this are going to start popping up in codebases consuming this change:

// this function only touches one or two properties of a larger interface
mockFoo.mockReturnValue({
  x: 42
} as Partial<RealReturnType> as RealReturnType);

mockFoo.mockImplementation((a, _b, _c, _iReallyDontCare, _makeItStop) => {
  return Promise.resolve(a)
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated description with breaking change

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Although the rest of the changes look excellent (really looking forward to being able to strictly type spies/mocks!), that one point of requiring explicit type definitions on variables is not a good thing for many users. IMO we should default to any when not provided, not never.

Agreed that it's often better practice to explicitly type things, but not all users are interested in best practices, or the same best practices. Some users are just interested in having tests work out of the box, and any added pain given to them by TypeScript is a negative - even if it gives better type checking. A few examples:

  • Large codebases converting from JavaScript would want as few places to need to explicitly type variables as possible.
  • In a lot of folks' minds, unit tests don't need nearly the same level of type checking as source code, and if you explicitly fail/error within a few milliseconds, needing to write & update them constantly just gets in the way.

Speaking more generally: what are "best practices"? If your goals are to have completely strict type safety, then sure, enforcing explicit types helps enforce your personal best practices. But not everybody has that same goal. Some just want to write code quickly, be able to define a few non-object constructs such as interfaces, and get some improved level of IDE features. IMO we shouldn't sacrifice one use case (stricter typing) for others (quicker development, easier TS migration).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand all your arguments and i think it's case by case, team by team, project by project.

  • Migration may require at the beginning an any default value.
  • Experienced people may not need to force them do it right when at the opposite other teams need it.

So i am calling a crazy idea.
How to make this opinionated default value customizable for each of us depending on the needs.

^^
=> joke or no 🍡
a cutomizable way freestyle
toggle line 8 comment

Plan B: to keep any as default in jest types
Projects can use the compiler flag noImplicitAny + the ts lint rule noAny.
I'll try it during the weekend.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did one test that show me a pure inconsistency
Example in image:
image

if the plan is to add only default value any for jest.Mock and jest.SpyInstance
ts error for the second test and the third one but not the first one => inconsistency
So i disagree with adding it back :) but i see that you insist on adding it back (hope you change your mind about it)

so i am fall backing to my plan b ^^ in the meantime

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noImplicitAny didn't work with generik default any :/ (plan b => fail)

plan c:
I found a way by wrapping the type (based on @jwbay idea)
type CustomSpyInstance<A = never, B extends any[] = never[]> = jest.SpyInstance<A, B>; which override any/any[] and i ban explicit usage of jest.SpyInstance type from code base.

I use it as follow:
let spy: CustomSpyInstance<number, [Hero[]]>;
or
// this one fail in spyOn to force typing - prevent devs from passing any types
let spy: CustomSpyInstance;

this can be done at the project level not impacting all the devs around the world (migrating from js to ts ;) )

new (...args: Y): T;
(...args: Y): T;
}

interface SpyInstance<T = {}> extends MockInstance<T> {}
interface SpyInstance<T, Y extends any[]> extends MockInstance<T, Y> {}

/**
* Wrap module with mock definitions
Expand All @@ -781,14 +782,14 @@ declare namespace jest {
* myApi.myApiMethod.mockImplementation(() => "test");
*/
type Mocked<T> = {
[P in keyof T]: T[P] & MockInstance<T[P]>;
[P in keyof T]: T[P] & MockInstance<T[P], ArgsType<T[P]>>;
} & T;

interface MockInstance<T> {
interface MockInstance<T, Y extends any[]> {
/** Returns the mock name string set by calling `mockFn.mockName(value)`. */
getMockName(): string;
/** Provides access to the mock's metadata */
mock: MockContext<T>;
mock: MockContext<T, Y>;
/**
* Resets all information stored in the mockFn.mock.calls and mockFn.mock.instances arrays.
*
Expand Down Expand Up @@ -828,7 +829,7 @@ declare namespace jest {
*
* Note: `jest.fn(implementation)` is a shorthand for `jest.fn().mockImplementation(implementation)`.
*/
mockImplementation(fn?: (...args: any[]) => any): Mock<T>;
mockImplementation(fn?: (...args: Y) => T): Mock<T, Y>;
/**
* Accepts a function that will be used as an implementation of the mock for one call to the mocked function.
* Can be chained so that multiple function calls produce different results.
Expand All @@ -844,9 +845,9 @@ declare namespace jest {
*
* myMockFn((err, val) => console.log(val)); // false
*/
mockImplementationOnce(fn: (...args: any[]) => any): Mock<T>;
mockImplementationOnce(fn: (...args: Y) => T): Mock<T, Y>;
/** Sets the name of the mock`. */
mockName(name: string): Mock<T>;
mockName(name: string): Mock<T, Y>;
/**
* Just a simple sugar function for:
*
Expand All @@ -856,7 +857,7 @@ declare namespace jest {
* return this;
* });
*/
mockReturnThis(): Mock<T>;
mockReturnThis(): Mock<T, Y>;
/**
* Accepts a value that will be returned whenever the mock function is called.
*
Expand All @@ -868,7 +869,7 @@ declare namespace jest {
* mock.mockReturnValue(43);
* mock(); // 43
*/
mockReturnValue(value: any): Mock<T>;
mockReturnValue(value: T): Mock<T, Y>;
/**
* Accepts a value that will be returned for one call to the mock function. Can be chained so that
* successive calls to the mock function return different values. When there are no more
Expand All @@ -885,11 +886,11 @@ declare namespace jest {
* console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
*
*/
mockReturnValueOnce(value: any): Mock<T>;
mockReturnValueOnce(value: T): Mock<T, Y>;
/**
* Simple sugar function for: `jest.fn().mockImplementation(() => Promise.resolve(value));`
*/
mockResolvedValue(value: any): Mock<T>;
mockResolvedValue(value: T | PromiseLike<T>): Mock<Promise<T>, Y>;
/**
* Simple sugar function for: `jest.fn().mockImplementationOnce(() => Promise.resolve(value));`
*
Expand All @@ -909,7 +910,7 @@ declare namespace jest {
* });
*
*/
mockResolvedValueOnce(value: any): Mock<T>;
mockResolvedValueOnce(value: T | PromiseLike<T>): Mock<Promise<T>, Y>;
/**
* Simple sugar function for: `jest.fn().mockImplementation(() => Promise.reject(value));`
*
Expand All @@ -921,7 +922,7 @@ declare namespace jest {
* await asyncMock(); // throws "Async error"
* });
*/
mockRejectedValue(value: any): Mock<T>;
mockRejectedValue(value: any): Mock<Promise<T>, Y>;

/**
* Simple sugar function for: `jest.fn().mockImplementationOnce(() => Promise.reject(value));`
Expand All @@ -939,26 +940,22 @@ declare namespace jest {
* });
*
*/
mockRejectedValueOnce(value: any): Mock<T>;
mockRejectedValueOnce(value: any): Mock<Promise<T>, Y>;
}

/**
* Represents the result of a single call to a mock function.
*/
interface MockResult {
type: 'return' | 'throw' | 'incomplete';
/**
* True if the function threw.
* False if the function returned.
*/
isThrow: boolean;
antoinebrault marked this conversation as resolved.
Show resolved Hide resolved
/**
* The value that was either thrown or returned by the function.
* The value that was either thrown or returned by the function, or undefined if type = 'incomplete'
*/
value: any;
}

interface MockContext<T> {
calls: any[][];
interface MockContext<T, Y extends any[]> {
calls: Y[];
instances: T[];
invocationCallOrder: number[];
/**
Expand Down