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

idea: .branded.inspect to view deep prop types #113

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

Conversation

mmkal
Copy link
Owner

@mmkal mmkal commented Aug 22, 2024

Use .branded.inspect to find badly-defined paths:

This finds any and never types deep within objects. This can be useful for debugging, since you will get autocomplete for the bad paths, but is a fairly heavy operation, so use with caution for large/complex types.

const bad = (metadata: string) => ({
  name: 'Bob',
  dob: new Date('1970-01-01'),
  meta: {
    raw: metadata,
    parsed: JSON.parse(metadata), // whoops, any!
  },
  exitCode: process.exit(), // whoops, never!
})

expectTypeOf(bad).returns.branded.inspect({
  foundProps: {
    '.meta.parsed': 'any',
    '.exitCode': 'never',
  },
})

const good = (metadata: string) => ({
  name: 'Bob',
  dob: new Date('1970-01-01'),
  meta: {
    raw: metadata,
    parsed: JSON.parse(metadata) as unknown, // here we just cast, but you should use zod/similar validation libraries
  },
  exitCode: 0,
})

expectTypeOf(good).returns.branded.inspect({
  foundProps: {},
})

expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({
  foundProps: {
    '.meta.parsed': 'unknown',
  },
})

PR notes: this also adds a nominalTypes option to DeepBrand. Reason being, a type like Date which has tons of methods can blow up the size of the type, and hurt performance/make us more likely to hit the dreaded "type instantiation is excessively deep" error. So now, by default DeepBrand is passed nominalTypes: {Date: Date} which basically means if it sees a type matching Date (using MutuallyExtends), it will just put {type: 'Date'} in that spot in the big branded schema thing, and stop recursing.

In theory people could configure this so they could do:

expectTypeOf<MyType>()
  .branded.configure<{
    nominalTypes: {
      Date: Date,
      S3Bucket: awscdk.s3.Bucket,
    },
  }>()
  .toEqualTypeOf<{foo: awscdk.s3.Bucket}>()

Which should improve performance and reliability.

But mostly I just added it to make sure Date didn't break the new inspect functionality. Probably needs more careful thinking.

This could go in post-v1 since it's non-breaking.

@mmkal mmkal changed the title idea: deep prop types idea: .inspect to view deep prop types Aug 22, 2024
Copy link

pkg-pr-new bot commented Aug 22, 2024

commit: ad379b1

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

Open in Stackblitz

Comment on lines +56 to +57
: Not<IsNever<NominalType<T, Options>>> extends true
? {type: NominalType<T, Options>}
Copy link
Owner Author

Choose a reason for hiding this comment

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

I'm sure there's a better way to do this without having to explicitly check for never/user NominalType twice

@@ -47,7 +47,7 @@ export type MismatchInfo<Actual, Expected> =
K extends keyof Expected ? Expected[K] : never
>
}
: StrictEqualUsingBranding<Actual, Expected> extends true
: StrictEqualUsingBranding<Actual, Expected, DeepBrandOptionsDefaults> extends true
Copy link
Owner Author

Choose a reason for hiding this comment

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

this is temp - we should make sure we respect whatever options the .branded that's calling this has mebe??

export type ExpectNullable<T> = {[expectNullable]: T; result: Not<StrictEqualUsingBranding<T, NonNullable<T>>>}
export type ExpectNullable<T> = {
[expectNullable]: T
result: Not<StrictEqualUsingBranding<T, NonNullable<T>, DeepBrandOptionsDefaults>>
Copy link
Owner Author

Choose a reason for hiding this comment

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

this is temp - we should make sure we respect whatever options the .branded that's calling this has mebe?? or should this not be using branding at all?

probably branding should always be opt-in since it can be slow

@mmkal mmkal changed the title idea: .inspect to view deep prop types idea: .branded.inspect to view deep prop types Aug 22, 2024
@aryaemami59
Copy link
Collaborator

Seems like this would have very niche use cases. What's the difference between:

expectTypeOf(bad).returns.branded.inspect({
  foundProps: {
    '.meta.parsed': 'any',
  },
})

and this:

expectTypeOf(bad)
  .returns.toHaveProperty('meta')
  .toHaveProperty('parsed')
  .toBeAny()

@mmkal
Copy link
Owner Author

mmkal commented Aug 25, 2024

It's basically DX. The toHaveProperty one is useful when you deliberately made meta.parsed any. The .branded.inspect one is useful if you have a big complex object and you want to make sure that there are no anys hiding in it - you don't know it's meta.parsed, it could be foo.bar.baz.abc.def or whatever.

So basically I think I'd only check anything in when foundProps is empty. But when something goes wrong, it'll find where the bad types are hiding.

@aryaemami59
Copy link
Collaborator

@mmkal That actually makes a lot of sense. I think an explanation similar to that should probably be included in the docs. Something like .toBeAny() vs .inspect().

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

Successfully merging this pull request may close these issues.

2 participants