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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]: Make asymmetric matcher types more useful #13812

Closed
benjaminjkraft opened this issue Jan 25, 2023 · 7 comments
Closed

[Feature]: Make asymmetric matcher types more useful #13812

benjaminjkraft opened this issue Jan 25, 2023 · 7 comments

Comments

@benjaminjkraft
Copy link
Contributor

馃殌 Feature Proposal

Using Jest expect's asymmetric matchers (anything, arrayContaining, etc.) in typed contexts gets very annoying very fast. For example:

function expectTypedEqual<T>(a: T, b: T) { expect(a).toEqual(b) }

// type error:
// Argument of type 'number[]' is not assignable to parameter of type 'AsymmetricMatcher_2'.
//   Property 'asymmetricMatch' is missing in type 'number[]' but required in type 'AsymmetricMatcher_2'.
expectTypedEqual([1, 2, 3], expect.arrayContaining(3))

This is in some sense correct: expect.arrayContaining(3) is not, in fact, a number[]. But in practice you almost always want to use it as a number[]! The proposal here is that we should lie about the types, and say it returns a number[], because that allows for greater type-safety in practice.

Motivation

Right now, most places where matchers get used in vanilla jest are untyped: for example toEqual takes (unknown, unknown). But it would be nice to support a move towards having more typed assertions as suggested in #13334; and this would also be useful for a first-party when as I just proposed in #13811. (In fact, I've mainly run into this when using an internal version of when.)

Example

No response

Pitch

This can't really be done anywhere else, but doing it in jest is just a matter of deciding the new types are better than the old ones, and adding them! This is a breaking change, but in practice it seems unlikely that many people are using the existing (unexported) types in a rich way.

@mrazauskas
Copy link
Contributor

There was somewhat similar attempt to "lie about the types" of asymmetric matchers in @types/jest (DefinitelyTyped/DefinitelyTyped#62831) and it was reverted (DefinitelyTyped/DefinitelyTyped#63151).

This can't really be done anywhere else

I still think this can be done and should be done in separate package. See: #13334 (comment). Glad to help, but I don鈥檛 want to create this package by myself. Simply because I can鈥檛 see myself maintaining a package which I do not use.

the existing (unexported) types

What do you have in mind?

@benjaminjkraft
Copy link
Contributor Author

benjaminjkraft commented Jan 27, 2023

Thanks, I totally forgot we can override the types of expect like that. I'm using the following types; I don't know that this is big enough to bother publishing as a package anyway but others can copy-paste this:

declare module "@jest/expect" {
	interface AsymmetricMatchers {
		any<T>(sample: T): T extends (...args: Array<unknown>) => infer V ? V : T
		anything(): any
		arrayContaining<T>(sample: T): Array<T>
		closeTo(sample: number, precision?: number): number
		objectContaining<T extends Record<string | number | symbol, unknown>, U extends T>(sample: T): U
		stringContaining(sample: string): string
		stringMatching(sample: string | RegExp): string
	}
}

@mrazauskas
Copy link
Contributor

mrazauskas commented Jan 28, 2023

That鈥檚 right.

Same with .toEqual(). You can simply augmentation the types instead of creating new expectTypedEqual() function.

@benjaminjkraft
Copy link
Contributor Author

For .toEqual it doesn't work! What we would want is to do something like

declare type Expect {
  <T>(actual: T): Matchers<void, T> & ...
} & ...
declare interface Matchers<R, T> {
  toEqual(expected: T): R
  ...
}

but the problem is we don't actually get access to T -- Matchers is just Matchers<R> and Expect looks more like

declare type Expect {
  <T>(actual: T): Matchers<void> & ...
} & ...

so our Matchers methods can't get access to the type of the argument of expect. (And Expect isn't an interface so we can't even extend its type to change this, not to mention the fact that that would be fairly fragile.) Would it make sense to change the types in Jest so Matchers gets access to T, even if the types in Jest itself don't actually use it?

BTW, another use case for this for us is to make the matchers be typescript assertions, so that for example after you do expect(v).toBeDefined() TypeScript will know that v can't be undefined. Right now we can't even do that on our own custom matchers, again because we don't have access to T.

@mrazauskas
Copy link
Contributor

Good point. I think it is reasonable to bring back Matchers<R, T>. It was there not so long time ago: #12404. Also having T around would be useful for custom matchers in general.

So would you be up to opening a PR?

benjaminjkraft added a commit to benjaminjkraft/jest that referenced this issue Jan 31, 2023
Matchers isn't as typed as some users would like (see jestjs#13334, jestjs#13812).
For users who want to customize it by extending the `Matchers`
interface, it's useful to have access to the type of `actual` (the
argument of `expect`) so you can do, say,
```ts
interface Matchers<R, T> {
    toTypedEqual(expected: T): R
}
```
This commit exposes it. The first-party matchers still have the same
types as before.
@benjaminjkraft
Copy link
Contributor Author

Sure can: #13848

@github-actions
Copy link

github-actions bot commented Mar 3, 2023

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Please note this issue tracker is not a help forum. We recommend using StackOverflow or our discord channel for questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 3, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants