diff --git a/README.md b/README.md index f9a1752b..70a21db7 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,57 @@ expectTypeOf<1 | null>().toBeNullable() expectTypeOf<1 | undefined | null>().toBeNullable() ``` +Use `.branded.inspect` to find badly-defined paths: + +This finds `any` and `never` types deep within objects. This can be useful for debugging, or for validating large or complex types. If there are `any` or `never` types lurking deep within the type, your IDE will highlight the bad paths. + +Note: this is a fairly heavy operation, so you might not want to actually commit the assertions to source control. + +```typescript +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', + }, +}) +``` + +You can use `.branded.inspect` to confirm there are no unexpected types: + +```typescript +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: {}, +}) + +// You can also use it to search for other types. Valid options for `findType` are currently only `'never' | 'any' | 'unknown'`. + +expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ + foundProps: { + '.meta.parsed': 'unknown', + }, +}) +``` + More `.not` examples: ```typescript diff --git a/src/branding.ts b/src/branding.ts index 3f5e2865..fcd97866 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -8,8 +8,29 @@ import type { OptionalKeys, MutuallyExtends, UnionToTuple, + IsTuple, + Not, + UnionToIntersection, + TupleToRecord, + IsRecord, } from './utils' +export type DeepBrandOptions = { + nominalTypes: {} +} + +export type DeepBrandOptionsDefaults = { + nominalTypes: { + Date: Date + } +} + +export type NominalType = Options['nominalTypes'] extends infer N + ? { + [K in keyof N]: MutuallyExtends extends true ? K : never + }[keyof N] + : never + /** * Represents a deeply branded type. * @@ -26,60 +47,155 @@ import type { * when you know you need it. If doing an equality check, it's almost always * better to use {@linkcode StrictEqualUsingTSInternalIdenticalToOperator}. */ -export type DeepBrand = +export type DeepBrand = IsNever extends true ? {type: 'never'} : IsAny extends true ? {type: 'any'} : IsUnknown extends true ? {type: 'unknown'} - : T extends string | number | boolean | symbol | bigint | null | undefined | void - ? { - type: 'primitive' - value: T - } - : T extends new (...args: any[]) => any + : Not>> extends true + ? {type: NominalType} + : T extends string | number | boolean | symbol | bigint | null | undefined | void ? { - type: 'constructor' - params: ConstructorOverloadParameters - instance: DeepBrand any>>> + type: 'primitive' + value: T } - : T extends (...args: infer P) => infer R // avoid functions with different params/return values matching - ? NumOverloads extends 1 - ? { - type: 'function' - params: DeepBrand

- return: DeepBrand - this: DeepBrand> - props: DeepBrand> - } - : UnionToTuple> extends infer OverloadsTuple + : T extends new (...args: any[]) => any + ? { + type: 'constructor' + params: ConstructorOverloadParameters + instance: DeepBrand any>>, Options> + } + : T extends (...args: infer P) => infer R // avoid functions with different params/return values matching + ? NumOverloads extends 1 ? { - type: 'overloads' - overloads: { - [K in keyof OverloadsTuple]: DeepBrand - } + type: 'function' + params: DeepBrand + return: DeepBrand + this: DeepBrand, Options> + props: DeepBrand, Options> } - : never - : T extends any[] - ? { - type: 'array' - items: { - [K in keyof T]: T[K] - } - } - : { - type: 'object' - properties: { - [K in keyof T]: DeepBrand - } - readonly: ReadonlyKeys - required: RequiredKeys - optional: OptionalKeys - constructorParams: DeepBrand> - } + : UnionToTuple> extends infer OverloadsTuple + ? { + type: 'overloads' + overloads: { + [K in keyof OverloadsTuple]: DeepBrand + } + } + : never + : T extends any[] + ? IsTuple extends true + ? { + type: 'tuple' + items: { + [K in keyof T]: DeepBrand + } + } + : { + type: 'array' + items: DeepBrand + } + : IsRecord extends true + ? { + type: 'record' + keys: keyof T + values: DeepBrand + } + : { + type: 'object' + properties: { + [K in keyof T]: DeepBrand + } + readonly: ReadonlyKeys + required: RequiredKeys + optional: OptionalKeys + constructorParams: ConstructorOverloadParameters extends infer P + ? IsNever

extends true + ? never + : DeepBrand + : never + } /** * Checks if two types are strictly equal using branding. */ -export type StrictEqualUsingBranding = MutuallyExtends, DeepBrand> +export type StrictEqualUsingBranding = MutuallyExtends< + DeepBrand, + DeepBrand +> + +/** + * @internal don't use this unless you are deeply familiar with it! + * + * Walks over a type `T`, assuming that it's the output of the {@linkcode DeepBrand} utility. It looks for leaf nodes looking like `{type: FindType}`. + * When it finds them, it merges them into a string->string record, keeping track of a rough representation of the path-location. + * For simple objects, this path will roughly match dot-prop notation but it also traverses into all the structures that `DeepBrand` can emit. + * But it also goes into overloads, function parameters, return types, etc. The output is an ugly intersection of objects along with a marker `{deepBrandLeafNode: true}` + * which is purely for internal use. The output should not be shown to end-users! + */ +type _DeepPropTypesOfBranded = + IsNever extends true + ? {} + : T extends string + ? {} + : T extends {type: FindType} + ? {[K in PathTo]: T['type']} & {deepBrandLeafNode: true} // deepBrandLeafNode marker helps us throw out lots of array props which we don't want to include + : T extends any[] + ? _DeepPropTypesOfBranded, PathTo, FindType> + : UnionToIntersection< + { + [K in keyof T]: Extract< + _DeepPropTypesOfBranded>}`, FindType>, + {deepBrandLeafNode: true} + > + }[keyof T] + > + +/** Required options for for {@linkcode DeepBrandPropNotes}. */ +export type DeepBrandPropNotesOptions = Partial & { + findType: 'any' | 'never' | 'unknown' +} +/** Default options for for {@linkcode DeepBrandPropNotes}. */ +export type DeepBrandPropNotesOptionsDefaults = {findType: 'any' | 'never'} + +/** + * For an input type `T`, finds all deeply-nested properties in the {@linkcode DeepBrand} representation of it. + * + * The output is a developer-readable shallow record of prop-path -> resolved type. + * @example + * ```ts + * type X = {a: any; b: boolean; c: {d: any}} + * const notes: DeepBrandPropNotes = { + * '.a': 'any', + * '.c.d': 'any', + * } + * ``` + */ +export type DeepBrandPropNotes = + _DeepPropTypesOfBranded, '', Options['findType']> extends infer X + ? {} extends X + ? Record // avoid letting `{'.propThatUsedToBeAny': 'any'}` still being accepted after it's fixed + : {[K in Exclude]: X[K]} + : never + +/** + * @internal + * + * Helper to coerce a type `K` that you are already pretty sure is a string because it camed from a `keyof T` type expression. + * Useful because sometimes TypeScript forgets that. + * When it's not a string or number, it will output a big ugly literal type `'UNEXPECTED_NON_LITERAL_PROP'` - try to avoid this! + */ +export type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PROP' + +/** + * Gets a sensible suffix to a property path for a {@linkcode DeepBrand} output type. + * `[number]` for arrays, empty string for objects, and parenthesised-input for anything else. + */ +type DeepBrandPropPathSuffix = T extends {type: string} + ? K extends 'items' + ? '[number]' + : K extends 'properties' + ? '' + : `(${Prop})` + : `.${Prop}` diff --git a/src/index.ts b/src/index.ts index 7bd7cb14..615c6237 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,11 @@ -import type {StrictEqualUsingBranding} from './branding' +import { + DeepBrandOptions, + DeepBrandOptionsDefaults, + StrictEqualUsingBranding, + DeepBrandPropNotes, + DeepBrandPropNotesOptions, + DeepBrandPropNotesOptionsDefaults, +} from './branding' import type { ExpectAny, ExpectArray, @@ -294,46 +301,82 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOfUsing generic type argument syntax - * ```ts - * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() - * - * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() - * ``` - * - * @example - * Using inferred type syntax by passing a value - * ```ts - * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) - * - * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) - * ``` - * - * @param MISMATCH - The mismatch arguments. - * @returns `true`. - */ - toEqualTypeOf: < - Expected extends StrictEqualUsingBranding extends true - ? unknown - : MismatchInfo, - >( - ...MISMATCH: MismatchArgs, true> - ) => true - } + branded: Branded +} + +export interface Branded { + /** + * Uses TypeScript's internal technique to check for type "identicalness". + * + * It will check if the types are fully equal to each other. + * It will not fail if two objects have different values, but the same type. + * It will fail however if an object is missing a property. + * + * **_Unexpected failure_**? For a more permissive but less performant + * check that accommodates for equivalent intersection types, + * use {@linkcode PositiveExpectTypeOf.branded | .branded.toEqualTypeOf()}. + * @see {@link https://github.com/mmkal/expect-type#why-is-my-assertion-failing | The documentation for details}. + * + * @example + * Using generic type argument syntax + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + * + * expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() + * ``` + * + * @example + * Using inferred type syntax by passing a value + * ```ts + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) + * + * expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) + * ``` + * + * @param MISMATCH - The mismatch arguments. + * @returns `true`. + */ + toEqualTypeOf: < + Expected extends StrictEqualUsingBranding extends true + ? unknown + : MismatchInfo, + >( + ...MISMATCH: MismatchArgs, true> + ) => true + + /** + * Walk an object to find all paths that are badly-defined - meaning, have `any` or `never` types. + * + * In most cases, this should be passed `{badlyDefinedPaths: []}`, and a type error will appear if there are any badly-deifned paths. + * + * @param params Explicitly supplied "badly-defined" paths. For a well-defined type with no issues, pass an empty list. The compiler will tell you if you're wrong! + * @returns true + * + * @example + * ```ts + * type BadType = {a: any; b: boolean; c: never; d: [0, any]; e: Array<{f: any; g: number}>} + * + * // \@ts-expect-error lots of `any`/`never` in this type, so you're not allowed to claim there are no badly-defined paths. + * expectTypeOf().inspect({badlyDefinedPaths: []}) + * expectTypeOf().inspect({ + * badlyDefinePaths: ['.a: any', '.c: never', '.d[1]: any', 'e[number].f: any'], + * }) + * ``` + * + * @example + * ```ts + * type GoodType = {b: boolean: c: string} + * + * expectTypeOf().inspect({badlyDefinedPaths: []}) + * ``` + */ + inspect: < + PropNoteOptions extends Exclude = DeepBrandPropNotesOptionsDefaults, + >(params: { + foundProps: DeepBrandPropNotes + }) => true + + configure(): Branded } /** @@ -1048,13 +1091,20 @@ export const expectTypeOf: _ExpectTypeOf = ( 'instance', 'guards', 'asserts', - 'branded', ] as const type Keys = keyof PositiveExpectTypeOf | keyof NegativeExpectTypeOf + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + function createBranded(): Branded<{}, any> { + return { + inspect: () => true, + configure: () => createBranded(), + toEqualTypeOf: fn, + } + } + type FunctionsDict = Record, any> const obj: FunctionsDict = { - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ toBeAny: fn, toBeUnknown: fn, toBeNever: fn, @@ -1083,7 +1133,11 @@ export const expectTypeOf: _ExpectTypeOf = ( omit: expectTypeOf, toHaveProperty: expectTypeOf, parameter: expectTypeOf, + get branded() { + return createBranded() + }, } + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ const getterProperties: readonly Keys[] = nonFunctionProperties getterProperties.forEach((prop: Keys) => Object.defineProperty(obj, prop, {get: () => expectTypeOf({})})) diff --git a/src/messages.ts b/src/messages.ts index 126dd6ce..f2d70659 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,4 +1,4 @@ -import type {StrictEqualUsingBranding} from './branding' +import {DeepBrandOptionsDefaults, StrictEqualUsingBranding} from './branding' import type { And, Extends, @@ -64,7 +64,7 @@ export type MismatchInfo = }, OptionalKeys > - : StrictEqualUsingBranding extends true + : StrictEqualUsingBranding extends true ? Actual : `Expected: ${PrintType}, Actual: ${PrintType>}` @@ -206,7 +206,7 @@ export type ExpectNever = {[expectNever]: T; result: IsNever} const expectNullable = Symbol('expectNullable') export type ExpectNullable = { [expectNullable]: T - result: Not>> + result: Not, DeepBrandOptionsDefaults>> } /** diff --git a/src/utils.ts b/src/utils.ts index 14c55f0b..dfca7515 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -228,7 +228,23 @@ export type TuplifyUnion> = */ export type UnionToTuple = TuplifyUnion -export type IsTuple = Or<[Extends, Extends]> +/** The numbers between 0 and 9 that you learned in school */ +export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + +/** + * e.g. `['a', 'b']` -> `{ 0: 'a', 1: 'b' }` + * Looks at the keys to see which look digit-like, so could do the wrong thing for types like + * `['a', 'b'] & {'1foo': string}` + */ +export type TupleToRecord = { + [K in keyof T as `${Extract}`]: T[K] +} + +/** `true` iff `T` is a tuple, as opposed to an indeterminate-length array */ +export type IsTuple = number extends T['length'] ? false : true + +/** `true` iff `T` is a record accepting any string keys, or accepting any number keys */ +export type IsRecord = Or<[Extends, Extends]> export type IsUnion = Not['length'], 1>> diff --git a/test/errors.test.ts b/test/errors.test.ts index 65d52563..c04fa57b 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -236,13 +236,15 @@ test('toMatchObjectType', () => { }) test('usage.test.ts', () => { + const originalUsageTestFile = fs.readFileSync(__dirname + '/usage.test.ts', 'utf8') + // first make sure there are no bugs, to avoid creating noisy snapshot diffs when I blindly run `pnpm test -- -u` + expect(tsFileErrors({filepath: 'test/usage.test.ts', content: originalUsageTestFile})).toEqual('') + // remove all `.not`s and `// @ts-expect-error`s from the main test file and snapshot the errors - const usageTestFile = fs - .readFileSync(__dirname + '/usage.test.ts') - .toString() + const sabotagedUsageTestFile = originalUsageTestFile .split('\n') .map(line => line.replace('// @ts-expect-error', '// error expected on next line:')) .map(line => line.replace('.not.', '.')) .join('\n') - expect(tsFileErrors({filepath: 'test/usage.test.ts', content: usageTestFile})).toMatchSnapshot() + expect(tsFileErrors({filepath: 'test/usage.test.ts', content: sabotagedUsageTestFile})).toMatchSnapshot() }) diff --git a/test/types.test.ts b/test/types.test.ts index bb42143b..39cadef2 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-duplicate-type-constituents */ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import {test} from 'vitest' +import {expect, test} from 'vitest' import * as a from '../src/index' import type {UnionToIntersection} from '../src/index' import type { @@ -48,16 +48,16 @@ test('boolean type logic', () => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() - expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() }) test(`never types don't sneak by`, () => { @@ -166,7 +166,7 @@ test('parity with IsExact from conditional-type-checks', () => { /** shim conditional-type-check's `assert` */ const assert = (_result: T) => true /** shim conditional-type-check's `IsExact` using `Equal` */ - type IsExact = a.StrictEqualUsingBranding + type IsExact = a.StrictEqualUsingBranding // basic test for `assert` shim: expectTypeOf(assert).toBeCallableWith(true) @@ -236,10 +236,14 @@ test('parity with IsExact from conditional-type-checks', () => { }) test('Equal works with functions', () => { - expectTypeOf void, () => string>>().toEqualTypeOf() - expectTypeOf void, (s: string) => void>>().toEqualTypeOf() expectTypeOf< - a.StrictEqualUsingBranding<() => () => () => void, () => (s: string) => () => void> + a.StrictEqualUsingBranding<() => void, () => string, a.DeepBrandOptionsDefaults> + >().toEqualTypeOf() + expectTypeOf< + a.StrictEqualUsingBranding<() => void, (s: string) => void, a.DeepBrandOptionsDefaults> + >().toEqualTypeOf() + expectTypeOf< + a.StrictEqualUsingBranding<() => () => () => void, () => (s: string) => () => void, a.DeepBrandOptionsDefaults> >().toEqualTypeOf() }) @@ -686,10 +690,16 @@ test('Works arounds tsc bug not handling intersected types for this form of equi // The workaround is the new optional .branded modifier. expectTypeOf<{foo: number} & {bar: string}>().branded.toEqualTypeOf<{foo: number; bar: string}>() expectTypeOf(one).branded.toEqualTypeOf() - // @ts-expect-error - expectTypeOf<{foo: number} & {bar: string}>().branded.not.toEqualTypeOf<{foo: number; bar: string}>() - // @ts-expect-error - expectTypeOf(one).branded.not.toEqualTypeOf(two) + const tryUseBrandedDotNot = () => + // @ts-expect-error + expectTypeOf<{foo: number} & {bar: string}>().branded.not.toEqualTypeOf<{foo: number; bar: string}>() + + expect(tryUseBrandedDotNot).toThrow() + const tryUseBrandedDotNot2 = () => + // @ts-expect-error + expectTypeOf(one).branded.not.toEqualTypeOf(two) + + expect(tryUseBrandedDotNot2).toThrow() }) test(".branded doesn't get tripped up by overloaded functions", () => { @@ -762,6 +772,14 @@ test('Distinguish between identical types that are AND`d together', () => { expectTypeOf<(() => 1) & {x: 1}>().not.toEqualTypeOf<() => 1>() }) +test('.branded with tuples', () => { + type A = {tuple: [1, unknown]} + type B = {tuple: [1, any]} + + // @ts-expect-error any vs unknown inside tuple + expectTypeOf().branded.toEqualTypeOf() +}) + test('limitations', () => { // these *shouldn't* fail, but kept here to document missing behaviours. Once fixed, remove the expect-error comments to make sure they can't regress // @ts-expect-error TypeScript can't handle the truth: https://github.com/expect-type/issues/5 https://github.com/microsoft/TypeScript/issues/50670 @@ -848,6 +866,62 @@ test('Overload edge cases', () => { expectTypeOf().returns.toEqualTypeOf<1>() }) +test('prop notes', () => { + type X = { + aa: any + bb: boolean + aa1: number[] + obj: { + oa: any + ob: boolean + } + aa2: Array<{x: number; y: any; z: never}> + nn: never + tt: [0, any, 2, never, 3] + oo: { + (a: any, b: any): any[] + (b: unknown[]): never + } + ff: (this: any, x: 1) => 2 + } + + const notes: a.DeepBrandPropNotes = { + '.aa': 'any', + '.obj.oa': 'any', + '.aa2[number].y': 'any', + '.aa2[number].z': 'never', + '.nn': 'never', + '.tt[number].1': 'any', + '.tt[number].3': 'never', + '.oo(overloads).0(params)[number].0': 'any', + '.oo(overloads).0(params)[number].1': 'any', + '.oo(overloads).0(return)[number]': 'any', + '.oo(overloads).1(return)': 'never', + '.ff(this)': 'any', + } + + expectTypeOf(notes).toHaveProperty('.aa') +}) + +test('inspect', () => { + expectTypeOf<{u: unknown}>().branded.inspect({foundProps: {}}) + // make sure if you do accidentally supply some, you're only allowed to supply an obvious error message + expectTypeOf<{u: unknown}>().branded.inspect({foundProps: {'.u': 'No flagged props found!'}}) + + expectTypeOf<{ + r: Record + }>().branded.inspect({ + // @ts-expect-error we should be forced to say that a record has any in its RHS + foundProps: { + // '.r(values)': 'any', // uncommenting this would remove the error + }, + }) + + expectTypeOf<{a: Record}>().branded.toEqualTypeOf<{ + a: {[K in string]: unknown} + }>() +}) + test('toMatchObjectType', () => { expectTypeOf<{a: number}>().toMatchObjectType<{a: number}>() expectTypeOf<{a: number}>().not.toMatchObjectType<{a: string}>() diff --git a/test/usage.test.ts b/test/usage.test.ts index 789b2637..7d15ea3e 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-process-exit */ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */ @@ -152,6 +153,56 @@ test('Nullable types', () => { expectTypeOf<1 | undefined | null>().toBeNullable() }) +/** + * This finds `any` and `never` types deep within objects. + * This can be useful for debugging, or for validating large or complex types. If there are `any` or `never` types + * lurking deep within the type, your IDE will highlight the bad paths. + * + * Note: this is a fairly heavy operation, so you might not want to actually commit the assertions to source control. + */ +test('Use `.branded.inspect` to find badly-defined paths', () => { + 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', + }, + }) +}) + +test('You can use `.branded.inspect` to confirm there are no unexpected types', () => { + 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: {}, + }) + + // You can also use it to search for other types. Valid options for `findType` are currently only `'never' | 'any' | 'unknown'`. + + expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ + foundProps: { + '.meta.parsed': 'unknown', + }, + }) +}) + test('More `.not` examples', () => { expectTypeOf(1).not.toBeUnknown() expectTypeOf(1).not.toBeAny()