vitest icon indicating copy to clipboard operation
vitest copied to clipboard

fix: switch ExpectStatic `any` types to `AsymmetricMatcher<unknown>`, with `DeeplyAllowMatchers<T>`

Open JoshuaKGoldberg opened this issue 11 months ago • 3 comments

Description

Switches the two any types to ~~unknown~~ AsymmetricMatcher<unknown>, and updated APIs such as toEqual<T> to deeply allow those using a new DeeplyAllowMatchers<T>.

Sending as a draft for reference.

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • [x] It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • [x] Ideally, include a test that fails without this PR but passes with it.
    • ~~I don't know how to best do this. Do you want a type test somewhere?~~ I added a types.test.ts file but am 100% sure I missed a much better way to do that
  • [x] Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.

Tests

  • [x] Run the tests with pnpm test:ci.

Documentation

  • [x] If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • [x] Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

JoshuaKGoldberg avatar Dec 03 '24 18:12 JoshuaKGoldberg

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
Latest commit 3ce8784f9b83f59e51b8c82f8924430c8b43ea3a
Latest deploy log https://app.netlify.com/sites/vitest-dev/deploys/674f4d9078b4770008db36bb
Deploy Preview https://deploy-preview-7016--vitest-dev.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

netlify[bot] avatar Dec 03 '24 18:12 netlify[bot]

How is this going to work?

expect(obj).toEqual<{
  id: string
  name: string
}>({
  id: expect.any(String),
  name: 'Amelia',
})

Error:

Type 'unknown' is not assignable to type 'string'.ts(2322)

sheremet-va avatar Dec 04 '24 14:12 sheremet-va

How is this going to work?

expect(obj).toEqual<{
  id: string
  name: string
}>({
  id: expect.any(String),
  name: 'Amelia',
})

Error:

Type 'unknown' is not assignable to type 'string'.ts(2322)

Aha! Updated to use a DeeplyAllowMatchers.

JoshuaKGoldberg avatar Dec 06 '24 18:12 JoshuaKGoldberg

Hey @JoshuaKGoldberg! I'd love to include this in 3.2. Could you add some tests to test/typescript? Or should I take over this PR? Thank you!

sheremet-va avatar May 15 '25 09:05 sheremet-va

That's great, thank you @sheremet-va! Yes please - I'm a bit swamped this month and don't think I'll have time to figure out the test/typescript directory & add tests soon.

JoshuaKGoldberg avatar May 16 '25 15:05 JoshuaKGoldberg

Hello Vite! I just bumped to 3.2.0 and I ran into the same problem as https://github.com/vitest-dev/vitest/pull/7016#issuecomment-2517674066

Here is my code (details omitted for clarity):

const expectedEventDraft: EventDraft = {
  timestamp: expect.any(Number)
};
expect(onSubmit).toHaveBeenCalledWith(expectedEventDraft);
type EventDraft = {
  readonly timestamp: number;
  // ... other uninteresting properties
}

With an error:

TS2322: Type AsymmetricMatcher<unknown, MatcherState> is not assignable to type number

What is the correct way to type this? I tried to use DeeplyAllowMatchers but it doesn't seem to be exported.

PavelVanecek avatar Jun 02 '25 23:06 PavelVanecek

What is the correct way to type this? I tried to use DeeplyAllowMatchers but it doesn't seem to be exported.

Looks like we should expose it, I will create a PR

sheremet-va avatar Jun 03 '25 06:06 sheremet-va

I feel this change is breaking some legit types starting at Vitest 3.2:

function expectMany<T>(value: { enabled: false } | { enable: true; data: T }) {
  // In Vitest 3.2:
  //
  // Argument of type '{ enabled: false; } | { enable: true; data: T; }' is not assignable to parameter of type '{ enabled: false | AsymmetricMatcher<unknown, MatcherState>; } | { enable: true | AsymmetricMatcher<unknown, MatcherState>; data: AsymmetricMatcher<...> | DeeplyAllowMatchers<...>; }'.
  //   Type '{ enable: true; data: T; }' is not assignable to type '{ enabled: false | AsymmetricMatcher<unknown, MatcherState>; } | { enable: true | AsymmetricMatcher<unknown, MatcherState>; data: AsymmetricMatcher<...> | DeeplyAllowMatchers<...>; }'.
  //     Type '{ enable: true; data: T; }' is not assignable to type '{ enable: true | AsymmetricMatcher<unknown, MatcherState>; data: AsymmetricMatcher<unknown, MatcherState> | DeeplyAllowMatchers<T>; }'.
  //       Types of property 'data' are incompatible.
  //         Type 'T' is not assignable to type 'AsymmetricMatcher<unknown, MatcherState> | DeeplyAllowMatchers<T>'.
  //           Type 'T' is not assignable to type 'AsymmetricMatcher<unknown, MatcherState>'.ts(2345)
  expect(value).toEqual(value);
}

dubzzz avatar Jun 03 '25 08:06 dubzzz

We heavily use the satisfies keyword in our tests. This change seems to make it impossible to use it.

An abstract example, which demonstrates the problem:

type Foo = {
    bar: string;
    foobar: {
        foo: string;
        bar: string;
    }[]
}
const mockedFoo = vi.mocked(mock<Foo>()) // mock is from https://github.com/eratio08/vitest-mock-extended - but the important part is the expect statement
expect(mockedFoo).toHaveBeenCalledWith({
    bar: "baz",
    foobar: expect.any(Array),
} satisfies Foo)

The error: Type 'AsymmetricMatcher<unknown, MatcherState>' is missing the following properties from type....

WtfJoke avatar Jun 04 '25 21:06 WtfJoke

We have some tests that pass expect.anything() to a constructor, now it is broken too. Typechecking now reports error of

TypeCheckError: Argument of type 'AsymmetricMatcher<unknown, MatcherState>' is not assignable to parameter of type 'AvroReadable'. Type 'AsymmetricMatcher<unknown, MatcherState>' is missing the following properties from type 'AvroReadable' position, read

Were we using expect.anything() improperly?

    const avroReaderStub = new AvroReader(expect.anything());
    vi.mocked(avroReaderStub.hasNext).mockReturnValue(true);

    const chunk = new Chunk(avroReaderStub as any, 0, 0, "log/00/2020/07/30/2300/");
    assert.equal(chunk.hasNext(), true);

    vi.mocked(avroReaderStub.hasNext).mockReturnValue(false);
    assert.equal(chunk.hasNext(), false);

https://github.com/Azure/azure-sdk-for-js/blob/6b4ce4831b0c572226f7de89c7c6f38ff325d415/sdk/storage/storage-blob-changefeed/test/chunk.spec.ts#L50-L59

jeremymeng avatar Jun 06 '25 01:06 jeremymeng

@jeremymeng Your usage doesn't look right. If it can just work by new AvroReader({} as any), then that's not how you use expect.anything() https://vitest.dev/api/expect.html#expect-anything

hi-ogawa avatar Jun 06 '25 02:06 hi-ogawa

@hi-ogawa Thanks! Right, I don't think we care about the constructor argument for this test. I fixed the test by replacing expect.anything() with an object.

jeremymeng avatar Jun 06 '25 17:06 jeremymeng