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

Current setup instructions for Vitest don't work for asymmetric matchers' typings #649

Closed
acidoxee opened this issue Sep 9, 2023 · 10 comments

Comments

@acidoxee
Copy link

acidoxee commented Sep 9, 2023

I am currently migrating away from Jest to Vitest. I used to have Jest tests that use jest-extended, hence I tried keeping jest-extended as well in the migrated tests with Vitest. I followed official instructions in the docs to setup jest-extended for Vitest. My tests are now fully implemented with Vitest and leverage jest-extended matchers as well. I've already went through previous issues/PRs regarding Vitest and its TS setup.

  • package version: 4.0.1
  • node version: 16.20.0 on reproduction repository (same problem on my prod, which is 18.17.1)
  • npm (or yarn) version: npm 9.4.2 on reproduction repository (same problem on my prod, which is yarn 1.22.19)

What happened: Runtime works flawlessly and all tests pass, as well as typings for "classic" matchers ✅ But typings of asymmetric matchers don't seem to work ❌ If I follow the official instructions from the docs, I don't have any completion for jest-extended's matchers when used in an asymmetric manner. For instance, TS says Property 'toBeNumber' does not exist on type 'ExpectStatic'.(2339).

Reproduction repository: https://stackblitz.com/edit/vitest-dev-vitest-rsfqnf?file=test%2Fextended.test.ts

Possible solution: I've tried a few things, with OK-ish results, but never perfect, and I'm unfortunately not familiar enough with the way TS module types work to be sure to provide an adequate solution that wouldn't break for other users.

Try 1

The first thing I tried was append the other part of the setup instructions in the jest-extended.d.ts file, for which the docs mention this is only for Vitest < 0.31.0. I understand that Vitest went from using a Vi namespace to a vitest module declaration (which is probably why the docs mention the version thing), so I've just added the interface AsymmetricMatchersContaining ..... declaration in the existing module declaration locally:

declare module 'vitest' {
  interface Assertion<T = any> extends CustomMatchers<T> {}
+ interface AsymmetricMatchersContaining extends CustomMatchers<any> {}
} 

This seems to work at first, and is closer to what Vitest itself suggests to write when extending matchers, since there are no more TS problems, but it just seems like so because jest-extended's matchers now appear to be typed any. There's also not any completion for those matchers, as you can see if you try typing one from scratch.

Reproduction: https://stackblitz.com/edit/vitest-dev-vitest-cwyxfl?file=test%2Fextended.test.ts

Try 2

I can't remember why I tried this, but I've then replaced the AsymmetricMatchersContaining extension like so:

declare module 'vitest' {
  interface Assertion<T = any> extends CustomMatchers<T> {}
- interface AsymmetricMatchersContaining extends CustomMatchers<any> {}
+ interface AsymmetricMatchersContaining extends Assertion {}
}

This now provides proper completion and types for jest-extended's asymmetric matchers, but now those asymmetric matchers' return types do not match with the expected object properties' types. For instance, notice how expect.toBeNumber() returns void, while the expected type is obviously number. If I replace expect.toBeNumber() by expect.any(Number), this now works perfectly.

Reproduction: https://stackblitz.com/edit/vitest-dev-vitest-ymmvae?file=test%2Fextended.test.ts

Finding the proper solution

I'm not sure what the correct solution would be. My hunch of using extends Assertion left me stunned and unsure as to why this "works", but then all typing assertions nicely provided by Vitest are breaking due to mismatches between the expected properties and what jest-extended's matchers apparently return. Maybe that's just because jest-extended's matchers were not meant to be used this way (type-wise)? I'd also like to find a setup that works if I also have to add other custom matchers than jest-extended's, which is actually my case (I simplified here for the sake of simplicity), the setup should make sure that jest-extended's matchers typings work but don't override those of other custom matchers. If you could help me figure out what (or if anything) is wrong and how we could make all of this work, I'd greatly appreciate it 🙏

@keeganwitt
Copy link
Collaborator

keeganwitt commented Sep 13, 2023

I added these instructions in #610, but I'm not a Vitest user and wasn't sure they were complete, so thank you for chiming in about where they need more work. Thank you also for the reproduction examples! It really helps someone like me that's kinda stumbling my way through Vitest stuff. I will take a look this weekend and see what I can come up with.

@cdierkens
Copy link
Contributor

Related issue I've just submitted: #652

I think the issue is that the CustomMatcher interface is not exported.

@cdierkens
Copy link
Contributor

PR to allow declaration merging: #653

@cdierkens
Copy link
Contributor

cdierkens commented Oct 9, 2023

@acidoxee I was able to get this working without the above PR.

Here is your updated reproduction. https://stackblitz.com/edit/vitest-dev-vitest-whsqfe?file=jest-extended.d.ts

Here's the jest-extended.d.ts file I'm currently using

import "jest-extended";
import "vitest";

declare module "vitest" {
  interface Assertion<T = any> extends CustomMatchers<T> {}
  interface AsymmetricMatchersContaining<T = any> extends CustomMatchers<T> {}
  interface ExpectStatic extends CustomMatchers {}
}

@cdierkens
Copy link
Contributor

@keeganwitt Here's vitest's docs if they are useful, though I get errors following those steps.

keeganwitt added a commit that referenced this issue Oct 9, 2023
Co-authored-by: Christopher Dierkens <cjdierkens@gmail.com>
@acidoxee
Copy link
Author

acidoxee commented Oct 9, 2023

That's a good start @cdierkens, thanks! Although I can't seem to make this work as soon as I add my own custom matchers (following the docs you've linked, which is exactly what I had followed on my own). This was kinda always the problem: the setup for my own matchers (exclusively) works perfectly, and so does the setup you're suggesting with only jest-extended's matchers. But how to make it work with both? I have a toMatchJSON and a toBeUUID custom matchers, for which I need to define types, and add jest-extended's ones also. That, I couldn't figure out for now.

@keeganwitt
Copy link
Collaborator

That's a good start @cdierkens, thanks! Although I can't seem to make this work as soon as I add my own custom matchers (following the docs you've linked, which is exactly what I had followed on my own). This was kinda always the problem: the setup for my own matchers (exclusively) works perfectly, and so does the setup you're suggesting with only jest-extended's matchers. But how to make it work with both? I have a toMatchJSON and a toBeUUID custom matchers, for which I need to define types, and add jest-extended's ones also. That, I couldn't figure out for now.

Would it work to define an interface for your custom matchers and extend that interface in addition to jest-extended's? Shooting a bit from the hip here, since I don't have a test project with this setup.

@cdierkens
Copy link
Contributor

@keeganwitt That worked for me!

Here's the updated stackblitz that shows it working: https://stackblitz.com/edit/vitest-dev-vitest-whsqfe?file=test%2Fextended.test.ts

import 'jest-extended';
import 'vitest';

interface MyCustomMatchers {
  toBeFoo(): any;
}
declare module 'vitest' {
  interface Assertion<T = any> extends CustomMatchers<T>, MyCustomMatchers {}
  interface AsymmetricMatchersContaining<T = any>
    extends CustomMatchers<T>,
      MyCustomMatchers {}
  interface ExpectStatic extends CustomMatchers, MyCustomMatchers {}
}

keeganwitt added a commit that referenced this issue Oct 9, 2023
Co-authored-by: Christopher Dierkens <cjdierkens@gmail.com>
@acidoxee
Copy link
Author

acidoxee commented Oct 9, 2023

OK yeah this time it seems to work nicely, well done @keeganwitt @cdierkens 👏 thanks a bunch for the help!

@Maxim-Mazurok
Copy link

Maxim-Mazurok commented Aug 26, 2024

Using T = unknown instead of T = any also seems to work fine.

Also here's my workaround to use the old toHaveBeenCalledOnceWith instead of new toHaveBeenCalledExactlyOnceWith temporarily, want to do find-and-replace in a separate PR to make it easier to review...

// inspired by https://jest-extended.jestcommunity.dev/docs/getting-started/setup#vitest-typescript-types-setup

import type jestExtended from "jest-extended";
import "vitest";

interface MyCustomMatchers<R> extends Record<string, unknown> {
  toHaveBeenCalledOnceWith: (typeof jestExtended<R>)["toHaveBeenCalledExactlyOnceWith"];
}
declare module "vitest" {
  interface Assertion<T = unknown> extends CustomMatchers<T>, MyCustomMatchers<T> {}
  interface AsymmetricMatchersContaining<T = unknown> extends CustomMatchers<T>, MyCustomMatchers<T> {}
  interface ExpectStatic extends CustomMatchers, MyCustomMatchers {}
}

Actually, looks like it doesn't work in all cases, unfortunately. Will patch it up instead for now...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants