idea: `.branded.inspect` to view deep prop types
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.
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()
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 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().