jest icon indicating copy to clipboard operation
jest copied to clipboard

[Feature]: Make asymmetric matcher types more useful

Open benjaminjkraft opened this issue 2 years ago • 1 comments

🚀 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.

benjaminjkraft avatar Jan 25 '23 00:01 benjaminjkraft

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

This can't really be done anywhere else

I still think this can be done and should be done in separate package. See: https://github.com/facebook/jest/issues/13334#issuecomment-1356308238. Glad to help, but I don’t want to create this package by myself. Simply because I can’t see myself maintaining a package which I do not use.

the existing (unexported) types

What do you have in mind?

mrazauskas avatar Jan 26 '23 09:01 mrazauskas

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
	}
}

benjaminjkraft avatar Jan 27 '23 23:01 benjaminjkraft

That’s right.

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

mrazauskas avatar Jan 28 '23 07:01 mrazauskas

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.

benjaminjkraft avatar Jan 30 '23 22:01 benjaminjkraft

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?

mrazauskas avatar Jan 31 '23 08:01 mrazauskas

Sure can: #13848

benjaminjkraft avatar Jan 31 '23 22:01 benjaminjkraft

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[bot] avatar Mar 03 '23 01:03 github-actions[bot]