expect-type icon indicating copy to clipboard operation
expect-type copied to clipboard

idea: `.branded.inspect` to view deep prop types

Open mmkal opened this issue 1 year ago • 4 comments

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 avatar Aug 22 '24 18:08 mmkal

Open in StackBlitz

npm i https://pkg.pr.new/mmkal/expect-type@113

commit: 00002a3

pkg-pr-new[bot] avatar Aug 22 '24 18:08 pkg-pr-new[bot]

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()

aryaemami59 avatar Aug 25 '24 01:08 aryaemami59

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.

mmkal avatar Aug 25 '24 10:08 mmkal

@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().

aryaemami59 avatar Aug 27 '24 01:08 aryaemami59