jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
44.26k stars 6.47k forks source link

[Feature]: Replace `@types/jest`'s `expect` implementation with `@jest/expect` #12424

Open SimenB opened 2 years ago

SimenB commented 2 years ago

๐Ÿš€ Feature Proposal

People might not want to use the globals (expect, test etc.) Jest provides (either because of ESM or just a preference). And because Jest is written in TypeScript, when doing e.g. import {expect} from '@jest/globals' all built in matchers are typed. However, if you use some matchers from the community (such as jest-extended), they augment the matchers from @types/jest instead of expect (or @jest/expect). If instead all libraries augmented the actual expect types, the imported matcher would be typed correctly.

However, this flips the problem - now people using the globals no longer get custom matchers typed! The solution to this problem is to make @jest/types use @jest/expect instead of shipping its own types for this.

This issue is for tracking the work that needs to be done before releasing Jest 28 stable so there's a single source of truth - the source code.


I have started: https://github.com/SimenB/DefinitelyTyped/tree/jest-expect

We need to make sure the tests pass. Most breakage is due to the tests using e.g. jest.InverseMatchers etc - I don't think that's super useful? I might be wrong though, in which case we should expose more of those types from expect and/or @jest/expect.

~(note that running tests is "impossible" until https://github.com/microsoft/DefinitelyTyped-tools/pull/411 is merged. I ran npm install manually inside types/jest to see errors in the tests in my IDE)~

/cc @mrazauskas

mrazauskas commented 2 years ago

Very good move. What I was playing with roughly looked like this:

  1. Types of all globals are exported from 'jest' (instead of '@jest/globals')
  2. index.d.ts inside '@types/jest':
declare const jest: import('jest').Jest;

declare const beforeAll: import('jest').TestFrameworkGlobals['beforeAll'];
declare const beforeEach: import('jest').TestFrameworkGlobals['beforeEach'];

declare const afterAll: import('jest').TestFrameworkGlobals['afterAll'];
declare const afterEach: import('jest').TestFrameworkGlobals['afterEach'];

declare const describe: import('jest').TestFrameworkGlobals['describe'];
declare const fdescribe: import('jest').TestFrameworkGlobals['fdescribe'];
declare const xdescribe: import('jest').TestFrameworkGlobals['xdescribe'];

declare const test: import('jest').TestFrameworkGlobals['test'];
declare const xtest: import('jest').TestFrameworkGlobals['xtest'];

declare const it: import('jest').TestFrameworkGlobals['it'];
declare const fit: import('jest').TestFrameworkGlobals['fit'];
declare const xit: import('jest').TestFrameworkGlobals['xit'];

declare const expect: import('jest').JestExpect;
  1. package.json inside '@types/jest':
"version": "28.0.0",
"peerDependencies": {
    "jest": "^28.0.0",
  }

The idea was to turn 'jest' into the single source of truth. There is no way using Jest without installing 'jest', but it is possible to install different versions of 'jest', '@jest/globals' and '@types/jest'.

So importing import {expect, jest} from 'jest' or augmenting through '@types/jest' should reference same type definition which are installed with that particular version of Jest.

Might be there is still a chance that something would drift, or overlap, or go wrong. What you think? (;

SimenB commented 2 years ago

Yeah, that's essentially what https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44365 does, and is what I want as end state. However, that loses a bunch of jest.Thing helpers, so not sure if it's the right move at this moment.

I'd like to start with just replacing parts of @types/jest, then if that works it can be "global only". And I'd like to start with expect since that's what custom matchers integrate with. ๐Ÿ™‚

mrazauskas commented 2 years ago

Perhaps we could try it out with some @types/jest-28-alpha? It just augments globals. Should work. Or?

I was playing jest.Thing helpers as well. Very easy to have them (;

mrazauskas commented 2 years ago

Couldnโ€™t recall in which of branches I had them. Something like this seems to be working (importing from current exports):

declare const jest: import('@jest/environment').Jest;

declare namespace jest {
  export type MockedFunction<T> = import('jest-mock').MockedFunction<T>;
  export type MockedClass<T> = import('jest-mock').MockedClass<T>;
  // ...
}
SimenB commented 2 years ago

Ah nice, that'd be awesome. Would also be cool if we had the same in Jest ๐Ÿ™‚

We should probably port over the type tests in DefinitivelyTyped to this repo well

SimenB commented 2 years ago

For anyone following along, we've replace our own usage of @types/jest with the types exported from @jest/globals and an extra package sticking Jest's globals into TS's globals: #13344.

I'd still love for @types/jest to work with @jest/expect, but that might just be https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62037 or something like it instead of having steps on the way

akphi commented 2 years ago

@SimenB I think I stumbled on this issue since I want to make jest-extended to work with @jest/globals. With this change, does it mean we can do some updates to https://github.com/jest-community/jest-extended/blob/main/types/index.d.ts and jest-extended would work nicely with @jest/globals?

SimenB commented 2 years ago

If this issue is resolved, yes ๐Ÿ™‚ As of now @types/jest doesn't get its matchers from expect

adam-rocska commented 1 year ago

This ticket is just like how I like a good bbq... low n slow...

mrazauskas commented 1 year ago

Actually there is no need to have @types/jest and to use it for re-exports. TypeScript picks up types from the following declaration in package.json:

{
  "devDependencies": {
    "@types/jest": "npm:@jest/test-globals@*"
  }
}

At the moment @jest/test-globals is private and it needs better name too. But in general I find this solution simple and even obvious. Well.. Thatโ€™s rather a discovery for me, to be honest ;D

mrazauskas commented 1 year ago

@SimenB What you think about the above idea?

I was thinking @jest/test-globals could be simply published as @jest/globals. In this case, current exports of @jest/globals should be moved to jest.

And package.json looks like this:

{
  "devDependencies": {
    "@types/jest": "npm:@jest/globals@*"
  }
}

Ah.. Of course, @types/jest gets deprecated. The end (;


Here is somewhat similar solution I tried to put together: https://github.com/mattphillips/jest-expect-message/pull/70

SimenB commented 1 year ago

jest-runtime depends on @jest/globals, so we'd end up polluting the global namespace by default, which I wanna avoid.

But if we could end up exporting the globals from the jest package, that'd be cool

mrazauskas commented 1 year ago

Right... Now I recall that we already talked about this in #12411

SimenB commented 1 year ago

If we started publishing @jest/test-globals - would @types/jest be able to just import that and have the global environment augmented?

We're still missing some types etc., but easy enough to add any missing later

mrazauskas commented 1 year ago

Importing @jest/test-globals into @types/jest is interesting idea, but it felt clumsy. This is because other @types libraries depend on @types/jest. The change is breaking for them.

In a way they should publish major releases. Does that mean that those libraries should also bump their majors? Will everyone agree? For instance, some @types/x-jest is referencing @types/jest. It supports Jest 27 and up. Reexported @jest/test-globals types means they have to give up supporting Jest 27 (or not)?

Might be I am overthinking. In any case, it sounded that:

"devDependencies": {
  "@types/jest": "npm:@jest/test-globals@*"
}

is a solution to provide global typings without even touching @types/jest package. This way @types/x-jest can choose to support @jest/test-globals at any time they find it comfortable.

SimenB commented 1 year ago

Hmm, yeah. Good question.

Is it breaking in a way that we should fix? I guess we could also set the version in the deps of the packages that depend on @types/jest?

Or if the other packages on DT only e.g. add new matchers, they'd ideally extend expect types directly (much of what this issue is about). Then the other types from @types/jest can still live there, but new matchers work regardless of where they're coming from.

mrazauskas commented 1 year ago

Hm.. if I get it right, the dependency on another @types package is set through /// <reference types="..." /> directive. Those are not versioned. Might be that is limitation of DT. Also can be I missed something.

A real case is here: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/heft-jest

Here jest.mocked() is added through a @types package to avoid importing it from ts-jest.

The jest.mocked() which ships with Jest has different signature. Hence switching to the build-in Jest types is breaking. I opened https://github.com/microsoft/rushstack/issues/3609 to find a solution, but there is no progress for a year.