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

toMatchObjectType + toExtend - replacements for toMatchTypeOf #126

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

mmkal
Copy link
Owner

@mmkal mmkal commented Oct 9, 2024

Closes #55
Closes #10
(if merged, collecting feedback first)

This introduces two new methods to be used instead of the ever-controversial toMatchTypeOf.

  • toExtend - does what toMatchTypeOf used to do continues to do - basically just check Actual extends Expected
  • toMatchObjectType - does a Pick<...> of actual, so you can do precise checks on a subset of your type's keys:
  • toMatchTypeOf is deprecated but won't be removed in the foreseeable future
type MyType = {readonly a: string; b: number; c: {some: {very: {complex: 'type'}}}; d?: boolean}

expectTypeOf<MyType>().toMatchObjectType<{a: string; b: number}>() // fails - forgot readonly
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; b?: number}>() // fails - b shouldn't be optional
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; d: boolean}>() // fails - d should be optional

expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; b: number}>() // passes
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; d?: boolean}>() // passes

Note: toMatchObjectType only accepts plain object expectations - no unions, no tuples, no primitives

I suspect most instances of .toMatchTypeOf could be happily replaced by .toMatchObjectType, and the word "object" being in the name makes it a bit clearer what's happening - and it is closer to a compile-time version of the jest .toMatchObject method.

In the remainder of cases, people can reach for toExtend - though this one should be more rarely used, because extends is a fairly weak assertion (and, in fact, you can achieve something similar with {} as MyType satisfies {a: string}, without using any library.

toMatchObjectType + toExtend are my best friends now
Copy link

pkg-pr-new bot commented Oct 9, 2024

Open in Stackblitz

pnpm add https://pkg.pr.new/mmkal/expect-type@126

commit: ab2cff4

@mrazauskas
Copy link
Contributor

I see b?: number as problem. Imagine having a type with b?: number and updating it to b: number. I would like to see a failing test just like with:

expectTypeOf<Pick<MyType, "a" | "b">>().toEqualTypeOf<{readonly a: string; b?: number}>() // fail

Also not sure if this was intended, because documentation says: "This is a strict check, but only on the subset of keys that are in both types." Or I missed something?

@mmkal
Copy link
Owner Author

mmkal commented Oct 9, 2024

Yep, optional properties are checked too. All the goodness from toEqualTypeOf, it's basically equivalent to manually doing Pick. Have updated the comment in the body. What do you think the docs should say to explain it better?

More examples below, all work as expected. I might see what some other people think of this change before adding a few more tests to make clearer what it does.

type MyType = {readonly a: string; b: number; c: {some: {very: {complex: 'type'}}}; d?: boolean}

expectTypeOf<MyType>().toMatchObjectType<{a: string; b: number}>() // fails - forgot readonly
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; b?: number}>() // fails - b shouldn't be optional
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; d: boolean}>() // fails - d should be optional

expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; b: number}>() // passes
expectTypeOf<MyType>().toMatchObjectType<{readonly a: string; d?: boolean}>() // passes

type BinaryOp = {
  (a: number, b: number): number
  (a: bigint, b: bigint): bigint
}

type Calculator = {add: BinaryOp; subtract: BinaryOp}

expectTypeOf<Calculator>().toMatchObjectType<{add: BinaryOp}>()
expectTypeOf<Calculator>().toMatchObjectType<{subtract: BinaryOp}>()
expectTypeOf<Calculator>().toMatchObjectType<{add: BinaryOp; subtract: BinaryOp}>()

expectTypeOf<Calculator>().toMatchObjectType<{add: {(a: number, b: number): number; (a: bigint, b: bigint): bigint}}>()

expectTypeOf<Calculator>().toMatchObjectType<{add: (a: number, b: number) => number}>() // fails - only one overload
expectTypeOf<Calculator>().toMatchObjectType<{add: (a: bigint, b: bigint) => bigint}>() // fails - only one overload

(also, noticed you're working on something similar https://github.com/tstyche/tstyche - looks nice! have you had a similar decision to make there?)

@mrazauskas
Copy link
Contributor

Ah.. There was a typo. Got it. As you noticed somewhere: it is hard to explain what 'match' means in type context. For me it feels like Pick is familiar and clear. I was simply wondering about your opinion.

(Yes, I had .toMatch in TSTyche. It is deprecated in favour of Pick / Omit. By the way, only the surface looks similar. Yes, expect() API is somewhat familiar, but types are compared programatically like in tsd. Only that the install size of TSTyche is just above 220kB.)

@aryaemami59
Copy link
Collaborator

aryaemami59 commented Oct 9, 2024

@mmkal Can .toExtend() be negated?

edit: nvm just saw that it can.

@aryaemami59
Copy link
Collaborator

Is .toExtend() basically the same as .toMatchTypeOf()? Is it an attempt to rename it to something less confusing or is it stricter as well?

@mmkal
Copy link
Owner Author

mmkal commented Oct 9, 2024

Is .toExtend() basically the same as .toMatchTypeOf()? Is it an attempt to rename it to something less confusing or is it stricter as well?

Yes it's the same. toMatchTypeOf would be deprecated by this change

src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
src/index.ts Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
@mmkal mmkal force-pushed the toMatchObjectType branch 2 times, most recently from 1934786 to 5971398 Compare November 15, 2024 17:07
@mmkal
Copy link
Owner Author

mmkal commented Nov 15, 2024

Update - it's now a "deep" pick, to match jest's toMatchObject. That enables this kind of thing:

const user = {
  email: 'a@b.com',
  name: 'John Doe',
  address: {street: '123 2nd St', city: 'New York', zip: '10001', state: 'NY', country: 'USA'},
}

expectTypeOf(user).toMatchObjectType<{name: string; address: {city: string}}>()

It also differentiates it vs a simple usage of Pick<...> - that would only work for top-level properties.

expectTypeOf({a: 1, b: 2}).toMatchObjectType<{a: number}>() // prefer this
```

`.toEqualTypeOf`, `.toMatchObjectType`, and `.toExtend` all fail on missing properties:
Copy link
Contributor

@mrazauskas mrazauskas Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as missing optional properties? (No time to try out. Sorry.)

Also I was wondering, why you don’t use .not in these examples? Because // @ts-expect-error makes these assertions pass also with older versions of this library (playground).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as missing optional properties? (No time to try out. Sorry.)

toEqualTypeOf and toMatchObjectType fail on missing optional properties, but toExtend doesn't. Added some docs + tests to cover this explicitly though.

why you don’t use .not in these examples

Just because these examples are essentially the docs and .not hasn't been introduced yet. I'd be open to reordering stuff to put .not higher up, you raise a good point - it's less prone to "wrong" errors.

Copy link
Contributor

@mrazauskas mrazauskas Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, it was my stupid mistake. I just copied this code to TS Playground and tried to mess up something. All worked really really well (including .not). Until.. I realised that this was because of // @ts-expect-error..

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