From b57a9df86f401dab94a10527830eb7d55425cb3f Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 10:51:13 -0400 Subject: [PATCH 01/24] DeepPropTypes --- README.md | 38 ++++++++++++++ src/branding.ts | 22 +++++--- src/index.ts | 31 ++++++++++- src/utils.ts | 128 +++++++++++++++++++++++++++++++++++++++++++++ test/types.test.ts | 54 +++++++++++++++++++ test/usage.test.ts | 35 +++++++++++++ 6 files changed, 301 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b148ed70..116ab3a0 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,44 @@ expectTypeOf<1 | null>().toBeNullable() expectTypeOf<1 | undefined | null>().toBeNullable() ``` +Use `.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. + +```typescript +expectTypeOf<{x: any}>().inspect({ + badlyDefinedPaths: ['.x: any'], +}) + +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.inspect({ + badlyDefinedPaths: ['.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 {foo: string}, // here we just cast, but you should use zod/similar validation libraries + }, + exitCode: 0, +}) + +expectTypeOf(good).returns.inspect({ + badlyDefinedPaths: [], +}) +``` + More `.not` examples: ```typescript diff --git a/src/branding.ts b/src/branding.ts index 1b4bd1f9..82956e1a 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -8,6 +8,7 @@ import { OptionalKeys, MutuallyExtends, UnionToTuple, + IsTuple, } from './utils' /** @@ -62,12 +63,17 @@ export type DeepBrand = } : never : T extends any[] - ? { - type: 'array' - items: { - [K in keyof T]: T[K] + ? IsTuple extends true + ? { + type: 'tuple' + items: { + [K in keyof T]: DeepBrand + } + } + : { + type: 'array' + items: DeepBrand } - } : { type: 'object' properties: { @@ -76,7 +82,11 @@ export type DeepBrand = readonly: ReadonlyKeys required: RequiredKeys optional: OptionalKeys - constructorParams: DeepBrand> + constructorParams: ConstructorOverloadParameters extends infer P + ? IsNever

extends true + ? never + : DeepBrand

+ : never } /** diff --git a/src/index.ts b/src/index.ts index a9c1d7c5..5068a2c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends} from './utils' +import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends, BadlyDefinedPaths} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version @@ -258,6 +258,34 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf, 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: (params: {badlyDefinedPaths: BadlyDefinedPaths}) => true } /** @@ -911,6 +939,7 @@ export const expectTypeOf: _ExpectTypeOf = ( toMatchTypeOf: fn, toEqualTypeOf: fn, toBeConstructibleWith: fn, + inspect: fn, toBeCallableWith: expectTypeOf, extract: expectTypeOf, exclude: expectTypeOf, diff --git a/src/utils.ts b/src/utils.ts index bd90880e..461d7d7b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import {DeepBrand} from './branding' + /** * Negates a boolean type. */ @@ -227,3 +229,129 @@ export type TuplifyUnion> = * Convert a union like `1 | 2 | 3` to a tuple like `[1, 2, 3]`. */ export type UnionToTuple = TuplifyUnion + +export type IsPrimitive = [T] extends [string] | [number] | [boolean] | [null] | [undefined] | [void] | [bigint] + ? true + : false + +type Entries = + IsNever extends true + ? [] + : IsNever extends true + ? [] + : UnionToTuple< + { + [K in keyof T]: [K, T[K]] + }[keyof T] + > + +export type TupleEntries = { + [K in keyof T]: [K, T[K]] +} + +export type ConcatTupleEntries = E extends [infer Head, ...infer Tail] + ? Head extends [string | number, string[]] + ? [...Head[1], ...ConcatTupleEntries] + : [] + : [] + +export type IsTuple = number extends T['length'] ? false : true + +export type BadlyDefinedPaths = + IsNever extends true + ? [`${PathTo}: never`] + : IsAny extends true + ? [`${PathTo}: any`] + : T extends any[] + ? IsTuple extends true + ? ConcatTupleEntries<{ + [K in keyof T]: [K, BadlyDefinedPaths}`>] + }> + : BadlyDefinedPaths + : Entries extends [[infer K, infer V], ...infer _Tail] + ? [ + ...BadlyDefinedPaths}`>, + ...BadlyDefinedPaths>, PathTo>, + ] + : [] + +export type BadlyDefinedDeepBrand = T extends {type: 'any' | 'never'} + ? [`${PathTo}: ${T['type']}`] + : never + +type OmitNever = { + [K in keyof T as IsNever extends true ? never : K]: T[K] +} + +export type DeepPropTypes = + IsNever extends true + ? [] + : T extends string + ? [] + : T extends {type: TypeName} + ? [`${PathTo}: ${T['type']}`] + : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` + ? Entries> extends [[infer K, infer V], ...infer _Tail] + ? [ + ...DeepPropTypes}`, TypeName>, + ...DeepPropTypes>, PathTo, TypeName>, + ] + : [] + : T extends any[] + ? ConcatTupleEntries<{ + [K in keyof T]: [K, DeepPropTypes}]`, TypeName>] + }> + : Entries extends [[infer K, infer V], ...infer _Tail] + ? [ + ...DeepPropTypes}`, TypeName>, + ...DeepPropTypes>, PathTo, TypeName>, + ] + : [] + +type PropPathSuffix = K extends 'properties' | 'items' ? `` : `(${Extract})` + +// type I2 = + +type X = { + aa: any + bb: boolean + aa1: number[] + 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 +} +type Dreal = DeepBrand //['properties']['foo']['overloads'] +type D = { + type: 'object' + properties: { + a: { + type: 'any' + } + } +} +type t = DeepPropTypes + +const f = (tt: Tt) => {} + +f('.properties.foo.overloads[0].params.items[0]: any') + +type e = Entries + +type T = D +type PathTo = '' + +type x = + Entries extends [[infer K, infer V], ...infer _Tail] + ? // ? [ + // // `${PathTo}.${K}`, // + // ...InstancesOf, + // // keyof V, + // ...InstancesOf, PathTo>, + // ] + 'yes' + : 'no' diff --git a/test/types.test.ts b/test/types.test.ts index 07b5d5ab..64fbf38c 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -762,6 +762,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 @@ -847,3 +855,49 @@ test('Overload edge cases', () => { expectTypeOf().parameters.toEqualTypeOf<[] | [1]>() expectTypeOf().returns.toEqualTypeOf<1>() }) + +test('BadlyDefinedPaths', () => { + const badPaths: a.BadlyDefinedPaths<{ + any: any + b: boolean + goodArray: number[] + badArray: Array<{x: number; y: any; z: never}> + n: never + tuple: [0, any, 2, never, 3] + }> = [ + '.any: any', + '.badArray[number].y: any', + '.badArray[number].z: never', + '.n: never', + '.tuple.1: any', + '.tuple.3: never', + ] + + expectTypeOf(badPaths).toBeArray() +}) + +test('InstancesOf', () => { + type X = { + any: any + b: boolean + goodArray: number[] + badArray: Array<{x: number; y: any; z: never}> + n: never + tuple: [0, any, 2, never, 3] + } + + const instancesOfAnyAndNever: a.DeepPropTypes = [ + '.any: any', + '.badArray[number].y: any', + '.badArray[number].z: never', + '.n: never', + '.tuple.1: any', + '.tuple.3: never', + // '.any: any', + // '.badArray[number].y: any', + // '.badArray[number].z: never', + // '.n: never', + // '.tuple.1: any', + // '.tuple.3: never', + ] +}) diff --git a/test/usage.test.ts b/test/usage.test.ts index a843ec0e..66e3f149 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 }] */ @@ -121,6 +122,40 @@ test('Nullable types', () => { expectTypeOf<1 | undefined | null>().toBeNullable() }) +/** + * 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. + */ +test('Use `.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.inspect({ + badlyDefinedPaths: ['.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 {foo: string}, // here we just cast, but you should use zod/similar validation libraries + }, + exitCode: 0, + }) + + expectTypeOf(good).returns.inspect({ + badlyDefinedPaths: [], + }) +}) + test('More `.not` examples', () => { expectTypeOf(1).not.toBeUnknown() expectTypeOf(1).not.toBeAny() From a626f1e9d3557b8885242b820dd4d7a579d2b28f Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 13:06:35 -0400 Subject: [PATCH 02/24] intersection instead of tuple --- src/index.ts | 15 ++++-- src/utils.ts | 141 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 103 insertions(+), 53 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5068a2c5..5aa06f54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {StrictEqualUsingBranding} from './branding' +import {DeepBrand, StrictEqualUsingBranding} from './branding' import { MismatchInfo, Scolder, @@ -23,7 +23,14 @@ import { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends, BadlyDefinedPaths} from './utils' +import { + StrictEqualUsingTSInternalIdenticalToOperator, + AValue, + MismatchArgs, + Extends, + BadlyDefinedPaths, + DeepPropTypes, +} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version @@ -285,7 +292,9 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf().inspect({badlyDefinedPaths: []}) * ``` */ - inspect: (params: {badlyDefinedPaths: BadlyDefinedPaths}) => true + inspect: (params: { + badProps: DeepPropTypes + }) => true } /** diff --git a/src/utils.ts b/src/utils.ts index 461d7d7b..113b5ca7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -251,64 +251,95 @@ export type TupleEntries = { export type ConcatTupleEntries = E extends [infer Head, ...infer Tail] ? Head extends [string | number, string[]] - ? [...Head[1], ...ConcatTupleEntries] - : [] - : [] + ? {'0': Head[1]} & ConcatTupleEntries + : {} + : {} export type IsTuple = number extends T['length'] ? false : true -export type BadlyDefinedPaths = +// export type BadlyDefinedPaths = +// IsNever extends true +// ? [`${PathTo}: never`] +// : IsAny extends true +// ? [`${PathTo}: any`] +// : T extends any[] +// ? IsTuple extends true +// ? ConcatTupleEntries<{ +// [K in keyof T]: [K, BadlyDefinedPaths}`>] +// }> +// : BadlyDefinedPaths +// : Entries extends [[infer K, infer V], ...infer _Tail] +// ? BadlyDefinedPaths}`> & +// ...BadlyDefinedPaths>, PathTo>, +// ] +// : [] + +// export type BadlyDefinedDeepBrand = T extends {type: 'any' | 'never'} +// ? [`${PathTo}: ${T['type']}`] +// : never + +type _DeepPropTypesOfBranded = IsNever extends true - ? [`${PathTo}: never`] - : IsAny extends true - ? [`${PathTo}: any`] - : T extends any[] - ? IsTuple extends true - ? ConcatTupleEntries<{ - [K in keyof T]: [K, BadlyDefinedPaths}`>] - }> - : BadlyDefinedPaths - : Entries extends [[infer K, infer V], ...infer _Tail] - ? [ - ...BadlyDefinedPaths}`>, - ...BadlyDefinedPaths>, PathTo>, - ] - : [] - -export type BadlyDefinedDeepBrand = T extends {type: 'any' | 'never'} - ? [`${PathTo}: ${T['type']}`] - : never - -type OmitNever = { - [K in keyof T as IsNever extends true ? never : K]: T[K] -} - -export type DeepPropTypes = - IsNever extends true - ? [] + ? {} : T extends string - ? [] + ? {} : T extends {type: TypeName} - ? [`${PathTo}: ${T['type']}`] + ? {[K in PathTo]: T['type']} & {gotem: true} : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? [ - ...DeepPropTypes}`, TypeName>, - ...DeepPropTypes>, PathTo, TypeName>, - ] - : [] + ? _DeepPropTypesOfBranded}`, TypeName> & + _DeepPropTypesOfBranded>, PathTo, TypeName> + : {} : T extends any[] - ? ConcatTupleEntries<{ - [K in keyof T]: [K, DeepPropTypes}]`, TypeName>] - }> - : Entries extends [[infer K, infer V], ...infer _Tail] - ? [ - ...DeepPropTypes}`, TypeName>, - ...DeepPropTypes>, PathTo, TypeName>, - ] - : [] - -type PropPathSuffix = K extends 'properties' | 'items' ? `` : `(${Extract})` + ? // ? ConcatTupleEntries<{ + // [K in keyof T]: [K, DeepPropTypes}]`, TypeName>] + // }> + _DeepPropTypesOfBranded, PathTo, TypeName> + : // UnionToIntersection<{ + // [K in Extract]: Extract< + // DeepPropTypes}`, TypeName>, + // {gotem: true} + // > + // }> + UnionToIntersection< + { + [K in keyof T]: Extract< + _DeepPropTypesOfBranded}`, TypeName>, + {gotem: true} + > + }[keyof T] + > + +export type DeepPropTypes = + _DeepPropTypesOfBranded, '', TypeName> extends X + ? { + [K in Exclude]: X[K] + } + : never + +type t2 = DeepPropTypes + +type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PROP' +type PropPathSuffix = K extends 'items' + ? '[number]' + : K extends 'properties' + ? '' + : `(${Extract})` + +type mytuple = [1, any, 2, never] + +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] +} + +type mytuplerecord = TupleToRecord // type I2 = @@ -316,6 +347,10 @@ 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] @@ -326,6 +361,13 @@ type X = { ff: (this: any, x: 1) => 2 } type Dreal = DeepBrand //['properties']['foo']['overloads'] + +export type GetEm = { + [K in Exclude]: T[K] +} + +type t = GetEm<_DeepPropTypesOfBranded> + type D = { type: 'object' properties: { @@ -334,7 +376,6 @@ type D = { } } } -type t = DeepPropTypes const f = (tt: Tt) => {} From eeb90fd9a657aa8c08b90ca19f6412c1c318b090 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 13:09:12 -0400 Subject: [PATCH 03/24] important infer --- src/utils.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 113b5ca7..8614a7d5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -311,7 +311,7 @@ type _DeepPropTypesOfBranded > export type DeepPropTypes = - _DeepPropTypesOfBranded, '', TypeName> extends X + _DeepPropTypesOfBranded, '', TypeName> extends infer X ? { [K in Exclude]: X[K] } @@ -377,10 +377,6 @@ type D = { } } -const f = (tt: Tt) => {} - -f('.properties.foo.overloads[0].params.items[0]: any') - type e = Entries type T = D From de07370259b1787fd3d563ebc4c742a971985d3f Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 13:09:29 -0400 Subject: [PATCH 04/24] try it, too deep --- test/usage.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/usage.test.ts b/test/usage.test.ts index 66e3f149..9527cc3e 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -138,7 +138,10 @@ test('Use `.inspect` to find badly-defined paths', () => { }) expectTypeOf(bad).returns.inspect({ - badlyDefinedPaths: ['.meta.parsed: any', '.exitCode: never'], + // badlyDefinedPaths: ['.meta.parsed: any', '.exitCode: never'], + badProps: { + '': '', + }, }) const good = (metadata: string) => ({ From 59c886c8ef7984af1f5ff880156de86d0ff4402d Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 13:37:33 -0400 Subject: [PATCH 05/24] nominal types --- src/branding.ts | 125 +++++++++++++++++++++++++++------------------ src/index.ts | 19 +++---- src/utils.ts | 19 ++++--- test/usage.test.ts | 8 +-- 4 files changed, 100 insertions(+), 71 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index 82956e1a..2d86cfa7 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -9,8 +9,25 @@ import { MutuallyExtends, UnionToTuple, IsTuple, + Not, } from './utils' +export type DeepBrandOptions = { + nominalTypes: {} +} + +export type DefaultDeepBrandOptions = { + 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. * @@ -27,69 +44,79 @@ import { * 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[] - ? IsTuple extends true - ? { - type: 'tuple' - items: { - [K in keyof T]: 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 } - } : { - type: 'array' - items: DeepBrand - } - : { - type: 'object' - properties: { - [K in keyof T]: 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 } - 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< + Left, + Right, + Options extends DefaultDeepBrandOptions = DefaultDeepBrandOptions, +> = MutuallyExtends, DeepBrand> + +type tt = DeepBrand<{d: Date}, DefaultDeepBrandOptions> + +type tn = NominalType<{d: Date}, DefaultDeepBrandOptions> // extends string ? [] : 1 diff --git a/src/index.ts b/src/index.ts index 5aa06f54..3c10a374 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {DeepBrand, StrictEqualUsingBranding} from './branding' +import {DeepBrandOptions, DefaultDeepBrandOptions, StrictEqualUsingBranding} from './branding' import { MismatchInfo, Scolder, @@ -23,14 +23,7 @@ import { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import { - StrictEqualUsingTSInternalIdenticalToOperator, - AValue, - MismatchArgs, - Extends, - BadlyDefinedPaths, - DeepPropTypes, -} from './utils' +import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends, DeepPropTypes} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version @@ -292,8 +285,12 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf().inspect({badlyDefinedPaths: []}) * ``` */ - inspect: (params: { - badProps: DeepPropTypes + inspect: < + Options extends DeepBrandOptions & {typeName: string} = DefaultDeepBrandOptions & { + typeName: 'any' | 'never' + }, + >(params: { + flaggedProps: DeepPropTypes }) => true } diff --git a/src/utils.ts b/src/utils.ts index 8614a7d5..cc7a8668 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import {DeepBrand} from './branding' +import {DeepBrand, DeepBrandOptions, DefaultDeepBrandOptions} from './branding' /** * Negates a boolean type. @@ -310,14 +310,19 @@ type _DeepPropTypesOfBranded }[keyof T] > -export type DeepPropTypes = - _DeepPropTypesOfBranded, '', TypeName> extends infer X - ? { - [K in Exclude]: X[K] - } +export type DeepPropTypes = + _DeepPropTypesOfBranded, '', Options['typeName']> extends infer X + ? {} extends X + ? Record // avoid letting `{'.propThatUsedToBeAny': 'any'}` still being accpeted after it's fixed + : {[K in Exclude]: X[K]} : never -type t2 = DeepPropTypes +type t2 = DeepPropTypes< + X, + DefaultDeepBrandOptions & { + typeName: 'any' | 'never' + } +> type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PROP' type PropPathSuffix = K extends 'items' diff --git a/test/usage.test.ts b/test/usage.test.ts index 9527cc3e..bfde893a 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -138,9 +138,9 @@ test('Use `.inspect` to find badly-defined paths', () => { }) expectTypeOf(bad).returns.inspect({ - // badlyDefinedPaths: ['.meta.parsed: any', '.exitCode: never'], - badProps: { - '': '', + flaggedProps: { + '.exitCode': 'never', + '.meta.parsed': 'any', }, }) @@ -155,7 +155,7 @@ test('Use `.inspect` to find badly-defined paths', () => { }) expectTypeOf(good).returns.inspect({ - badlyDefinedPaths: [], + flaggedProps: {}, }) }) From 2a92d1918cabc1a19ac12d5ff077c88213a12d12 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 13:51:02 -0400 Subject: [PATCH 06/24] configurable .branded --- src/branding.ts | 15 ++++--- src/index.ts | 102 +++++++++++++++++++++++++-------------------- src/utils.ts | 31 +++++++------- test/types.test.ts | 2 +- test/usage.test.ts | 20 +++++---- 5 files changed, 94 insertions(+), 76 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index 2d86cfa7..b4933522 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -16,7 +16,7 @@ export type DeepBrandOptions = { nominalTypes: {} } -export type DefaultDeepBrandOptions = { +export type DeepBrandOptionsDefaults = { nominalTypes: { Date: Date } @@ -111,12 +111,11 @@ export type DeepBrand = /** * Checks if two types are strictly equal using branding. */ -export type StrictEqualUsingBranding< - Left, - Right, - Options extends DefaultDeepBrandOptions = DefaultDeepBrandOptions, -> = MutuallyExtends, DeepBrand> +export type StrictEqualUsingBranding = MutuallyExtends< + DeepBrand, + DeepBrand +> -type tt = DeepBrand<{d: Date}, DefaultDeepBrandOptions> +type tt = DeepBrand<{d: Date}, DeepBrandOptionsDefaults> -type tn = NominalType<{d: Date}, DefaultDeepBrandOptions> // extends string ? [] : 1 +type tn = NominalType<{d: Date}, DeepBrandOptionsDefaults> // extends string ? [] : 1 diff --git a/src/index.ts b/src/index.ts index 3c10a374..55672100 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import {DeepBrandOptions, DefaultDeepBrandOptions, StrictEqualUsingBranding} from './branding' +import {DeepBrandOptions, DeepBrandOptionsDefaults, StrictEqualUsingBranding} from './branding' import { MismatchInfo, Scolder, @@ -23,7 +23,15 @@ import { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends, DeepPropTypes} from './utils' +import { + StrictEqualUsingTSInternalIdenticalToOperator, + AValue, + MismatchArgs, + Extends, + DeepBrandPropNotes, + DeepBrandPropNotesOptions, + DeepBrandPropNotesOptionsDefaults, +} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version @@ -218,46 +226,48 @@ 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. @@ -286,12 +296,12 @@ export interface PositiveExpectTypeOf extends BaseExpectTypeOf = DeepBrandPropNotesOptionsDefaults, >(params: { - flaggedProps: DeepPropTypes + notableProps: DeepBrandPropNotes }) => true + + configure(): Branded } /** diff --git a/src/utils.ts b/src/utils.ts index cc7a8668..71e0a157 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import {DeepBrand, DeepBrandOptions, DefaultDeepBrandOptions} from './branding' +import {DeepBrand, DeepBrandOptions, DeepBrandOptionsDefaults as DeepBrandOptionsDefaults} from './branding' /** * Negates a boolean type. @@ -278,23 +278,23 @@ export type IsTuple = number extends T['length'] ? fal // ? [`${PathTo}: ${T['type']}`] // : never -type _DeepPropTypesOfBranded = +type _DeepPropTypesOfBranded = IsNever extends true ? {} : T extends string ? {} - : T extends {type: TypeName} + : T extends {type: NotableType} ? {[K in PathTo]: T['type']} & {gotem: true} : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? _DeepPropTypesOfBranded}`, TypeName> & - _DeepPropTypesOfBranded>, PathTo, TypeName> + ? _DeepPropTypesOfBranded}`, NotableType> & + _DeepPropTypesOfBranded>, PathTo, NotableType> : {} : T extends any[] ? // ? ConcatTupleEntries<{ // [K in keyof T]: [K, DeepPropTypes}]`, TypeName>] // }> - _DeepPropTypesOfBranded, PathTo, TypeName> + _DeepPropTypesOfBranded, PathTo, NotableType> : // UnionToIntersection<{ // [K in Extract]: Extract< // DeepPropTypes}`, TypeName>, @@ -304,23 +304,26 @@ type _DeepPropTypesOfBranded UnionToIntersection< { [K in keyof T]: Extract< - _DeepPropTypesOfBranded}`, TypeName>, + _DeepPropTypesOfBranded}`, NotableType>, {gotem: true} > }[keyof T] > -export type DeepPropTypes = - _DeepPropTypesOfBranded, '', Options['typeName']> extends infer X +export type DeepBrandPropNotesOptions = Partial & {notable: string} +export type DeepBrandPropNotesOptionsDefaults = {notable: 'any' | 'never'} + +export type DeepBrandPropNotes = + _DeepPropTypesOfBranded, '', Options['notable']> extends infer X ? {} extends X - ? Record // avoid letting `{'.propThatUsedToBeAny': 'any'}` still being accpeted after it's fixed + ? Record // avoid letting `{'.propThatUsedToBeAny': 'any'}` still being accepted after it's fixed : {[K in Exclude]: X[K]} : never -type t2 = DeepPropTypes< +type t2 = DeepBrandPropNotes< X, - DefaultDeepBrandOptions & { - typeName: 'any' | 'never' + DeepBrandOptionsDefaults & { + notable: 'any' | 'never' } > @@ -365,7 +368,7 @@ type X = { } ff: (this: any, x: 1) => 2 } -type Dreal = DeepBrand //['properties']['foo']['overloads'] +type Dreal = DeepBrand //['properties']['foo']['overloads'] export type GetEm = { [K in Exclude]: T[K] diff --git a/test/types.test.ts b/test/types.test.ts index 64fbf38c..efe235b2 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -886,7 +886,7 @@ test('InstancesOf', () => { tuple: [0, any, 2, never, 3] } - const instancesOfAnyAndNever: a.DeepPropTypes = [ + const instancesOfAnyAndNever: a.DeepBrandPropNotes = [ '.any: any', '.badArray[number].y: any', '.badArray[number].z: never', diff --git a/test/usage.test.ts b/test/usage.test.ts index bfde893a..a7d7b91a 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -3,7 +3,7 @@ /* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */ import {test} from 'vitest' -import {expectTypeOf} from '../src' +import {DeepBrandOptionsDefaults, expectTypeOf} from '../src' test("Check an object's type with `.toEqualTypeOf`", () => { expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>() @@ -137,10 +137,10 @@ test('Use `.inspect` to find badly-defined paths', () => { exitCode: process.exit(), // whoops, never! }) - expectTypeOf(bad).returns.inspect({ - flaggedProps: { - '.exitCode': 'never', + expectTypeOf(bad).returns.branded.inspect({ + notableProps: { '.meta.parsed': 'any', + '.exitCode': 'never', }, }) @@ -149,13 +149,19 @@ test('Use `.inspect` to find badly-defined paths', () => { dob: new Date('1970-01-01'), meta: { raw: metadata, - parsed: JSON.parse(metadata) as {foo: string}, // here we just cast, but you should use zod/similar validation libraries + parsed: JSON.parse(metadata) as unknown, // here we just cast, but you should use zod/similar validation libraries }, exitCode: 0, }) - expectTypeOf(good).returns.inspect({ - flaggedProps: {}, + expectTypeOf(good).returns.branded.inspect({ + notableProps: {}, + }) + + expectTypeOf(good).returns.branded.inspect<{notable: 'unknown'}>({ + notableProps: { + '.meta.parsed': 'unknown', + }, }) }) From 4672cf20232952a1fe1f2d277a78c289597786d5 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 14:33:45 -0400 Subject: [PATCH 07/24] some cleanup --- README.md | 23 +++++++----- src/utils.ts | 89 +++++----------------------------------------- test/types.test.ts | 77 ++++++++++++++++++--------------------- 3 files changed, 57 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 116ab3a0..e0c1492d 100644 --- a/README.md +++ b/README.md @@ -216,10 +216,6 @@ Use `.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. ```typescript -expectTypeOf<{x: any}>().inspect({ - badlyDefinedPaths: ['.x: any'], -}) - const bad = (metadata: string) => ({ name: 'Bob', dob: new Date('1970-01-01'), @@ -230,8 +226,11 @@ const bad = (metadata: string) => ({ exitCode: process.exit(), // whoops, never! }) -expectTypeOf(bad).returns.inspect({ - badlyDefinedPaths: ['.meta.parsed: any', '.exitCode: never'], +expectTypeOf(bad).returns.branded.inspect({ + notableProps: { + '.meta.parsed': 'any', + '.exitCode': 'never', + }, }) const good = (metadata: string) => ({ @@ -239,13 +238,19 @@ const good = (metadata: string) => ({ dob: new Date('1970-01-01'), meta: { raw: metadata, - parsed: JSON.parse(metadata) as {foo: string}, // here we just cast, but you should use zod/similar validation libraries + parsed: JSON.parse(metadata) as unknown, // here we just cast, but you should use zod/similar validation libraries }, exitCode: 0, }) -expectTypeOf(good).returns.inspect({ - badlyDefinedPaths: [], +expectTypeOf(good).returns.branded.inspect({ + notableProps: {}, +}) + +expectTypeOf(good).returns.branded.inspect<{notable: 'unknown'}>({ + notableProps: { + '.meta.parsed': 'unknown', + }, }) ``` diff --git a/src/utils.ts b/src/utils.ts index 71e0a157..715f22f8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -278,33 +278,24 @@ export type IsTuple = number extends T['length'] ? fal // ? [`${PathTo}: ${T['type']}`] // : never -type _DeepPropTypesOfBranded = +type _DeepPropTypesOfBranded = IsNever extends true ? {} : T extends string ? {} - : T extends {type: NotableType} + : T extends {type: FindType} ? {[K in PathTo]: T['type']} & {gotem: true} : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? _DeepPropTypesOfBranded}`, NotableType> & - _DeepPropTypesOfBranded>, PathTo, NotableType> + ? _DeepPropTypesOfBranded}`, FindType> & + _DeepPropTypesOfBranded>, PathTo, FindType> : {} : T extends any[] - ? // ? ConcatTupleEntries<{ - // [K in keyof T]: [K, DeepPropTypes}]`, TypeName>] - // }> - _DeepPropTypesOfBranded, PathTo, NotableType> - : // UnionToIntersection<{ - // [K in Extract]: Extract< - // DeepPropTypes}`, TypeName>, - // {gotem: true} - // > - // }> - UnionToIntersection< + ? _DeepPropTypesOfBranded, PathTo, FindType> + : UnionToIntersection< { [K in keyof T]: Extract< - _DeepPropTypesOfBranded}`, NotableType>, + _DeepPropTypesOfBranded}`, FindType>, {gotem: true} > }[keyof T] @@ -320,22 +311,14 @@ export type DeepBrandPropNotes = : {[K in Exclude]: X[K]} : never -type t2 = DeepBrandPropNotes< - X, - DeepBrandOptionsDefaults & { - notable: 'any' | 'never' - } -> +export type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PROP' -type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PROP' -type PropPathSuffix = K extends 'items' +export type PropPathSuffix = K extends 'items' ? '[number]' : K extends 'properties' ? '' : `(${Extract})` -type mytuple = [1, any, 2, never] - export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' /** @@ -346,57 +329,3 @@ export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' export type TupleToRecord = { [K in keyof T as `${Extract}`]: T[K] } - -type mytuplerecord = TupleToRecord - -// type I2 = - -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 -} -type Dreal = DeepBrand //['properties']['foo']['overloads'] - -export type GetEm = { - [K in Exclude]: T[K] -} - -type t = GetEm<_DeepPropTypesOfBranded> - -type D = { - type: 'object' - properties: { - a: { - type: 'any' - } - } -} - -type e = Entries - -type T = D -type PathTo = '' - -type x = - Entries extends [[infer K, infer V], ...infer _Tail] - ? // ? [ - // // `${PathTo}.${K}`, // - // ...InstancesOf, - // // keyof V, - // ...InstancesOf, PathTo>, - // ] - 'yes' - : 'no' diff --git a/test/types.test.ts b/test/types.test.ts index efe235b2..a8a7b586 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -856,48 +856,39 @@ test('Overload edge cases', () => { expectTypeOf().returns.toEqualTypeOf<1>() }) -test('BadlyDefinedPaths', () => { - const badPaths: a.BadlyDefinedPaths<{ - any: any - b: boolean - goodArray: number[] - badArray: Array<{x: number; y: any; z: never}> - n: never - tuple: [0, any, 2, never, 3] - }> = [ - '.any: any', - '.badArray[number].y: any', - '.badArray[number].z: never', - '.n: never', - '.tuple.1: any', - '.tuple.3: never', - ] - - expectTypeOf(badPaths).toBeArray() -}) - -test('InstancesOf', () => { +test('prop notes', () => { type X = { - any: any - b: boolean - goodArray: number[] - badArray: Array<{x: number; y: any; z: never}> - n: never - tuple: [0, any, 2, never, 3] - } - - const instancesOfAnyAndNever: a.DeepBrandPropNotes = [ - '.any: any', - '.badArray[number].y: any', - '.badArray[number].z: never', - '.n: never', - '.tuple.1: any', - '.tuple.3: never', - // '.any: any', - // '.badArray[number].y: any', - // '.badArray[number].z: never', - // '.n: never', - // '.tuple.1: any', - // '.tuple.3: never', - ] + 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') }) From ef11530b452f315480021064053f70a292398624 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 14:39:45 -0400 Subject: [PATCH 08/24] some type errors --- README.md | 6 +++--- src/index.ts | 3 +-- src/messages.ts | 9 ++++++--- src/utils.ts | 6 +++--- test/types.test.ts | 40 +++++++++++++++++++++++++--------------- test/usage.test.ts | 8 ++++---- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index e0c1492d..872635dd 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ const bad = (metadata: string) => ({ }) expectTypeOf(bad).returns.branded.inspect({ - notableProps: { + foundProps: { '.meta.parsed': 'any', '.exitCode': 'never', }, @@ -244,11 +244,11 @@ const good = (metadata: string) => ({ }) expectTypeOf(good).returns.branded.inspect({ - notableProps: {}, + foundProps: {}, }) expectTypeOf(good).returns.branded.inspect<{notable: 'unknown'}>({ - notableProps: { + foundProps: { '.meta.parsed': 'unknown', }, }) diff --git a/src/index.ts b/src/index.ts index 55672100..cd6cc44a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -298,7 +298,7 @@ export interface Branded { inspect: < PropNoteOptions extends Exclude = DeepBrandPropNotesOptionsDefaults, >(params: { - notableProps: DeepBrandPropNotes + foundProps: DeepBrandPropNotes }) => true configure(): Branded @@ -955,7 +955,6 @@ export const expectTypeOf: _ExpectTypeOf = ( toMatchTypeOf: fn, toEqualTypeOf: fn, toBeConstructibleWith: fn, - inspect: fn, toBeCallableWith: expectTypeOf, extract: expectTypeOf, exclude: expectTypeOf, diff --git a/src/messages.ts b/src/messages.ts index 81d2de17..ced017c9 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -1,4 +1,4 @@ -import {StrictEqualUsingBranding} from './branding' +import {DeepBrandOptionsDefaults, StrictEqualUsingBranding} from './branding' import {And, Extends, Not, IsAny, UsefulKeys, ExtendsExcludingAnyOrNever, IsUnknown, IsNever} from './utils' /** @@ -47,7 +47,7 @@ export type MismatchInfo = K extends keyof Expected ? Expected[K] : never > } - : StrictEqualUsingBranding extends true + : StrictEqualUsingBranding extends true ? Actual : `Expected: ${PrintType}, Actual: ${PrintType>}` @@ -143,7 +143,10 @@ export type ExpectNever = {[expectNever]: T; result: IsNever} * @internal */ const expectNullable = Symbol('expectNullable') -export type ExpectNullable = {[expectNullable]: T; result: Not>>} +export type ExpectNullable = { + [expectNullable]: T + result: Not, DeepBrandOptionsDefaults>> +} /** * Checks if the result of an expecter matches the specified options, and diff --git a/src/utils.ts b/src/utils.ts index 715f22f8..326e91a0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -301,11 +301,11 @@ type _DeepPropTypesOfBranded }[keyof T] > -export type DeepBrandPropNotesOptions = Partial & {notable: string} -export type DeepBrandPropNotesOptionsDefaults = {notable: 'any' | 'never'} +export type DeepBrandPropNotesOptions = Partial & {findType: string} +export type DeepBrandPropNotesOptionsDefaults = {findType: 'any' | 'never'} export type DeepBrandPropNotes = - _DeepPropTypesOfBranded, '', Options['notable']> extends infer X + _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]} diff --git a/test/types.test.ts b/test/types.test.ts index a8a7b586..cdbe673c 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -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() }) @@ -875,7 +879,7 @@ test('prop notes', () => { ff: (this: any, x: 1) => 2 } - const notes: a.DeepBrandPropNotes = { + const notes: a.DeepBrandPropNotes = { '.aa': 'any', '.obj.oa': 'any', '.aa2[number].y': 'any', @@ -892,3 +896,9 @@ test('prop notes', () => { 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!'}}) +}) diff --git a/test/usage.test.ts b/test/usage.test.ts index a7d7b91a..7570d5d2 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -138,7 +138,7 @@ test('Use `.inspect` to find badly-defined paths', () => { }) expectTypeOf(bad).returns.branded.inspect({ - notableProps: { + foundProps: { '.meta.parsed': 'any', '.exitCode': 'never', }, @@ -155,11 +155,11 @@ test('Use `.inspect` to find badly-defined paths', () => { }) expectTypeOf(good).returns.branded.inspect({ - notableProps: {}, + foundProps: {}, }) - expectTypeOf(good).returns.branded.inspect<{notable: 'unknown'}>({ - notableProps: { + expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ + foundProps: { '.meta.parsed': 'unknown', }, }) From f56b375961951baf558deb85ecc2781b3fea3dad Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 14:42:56 -0400 Subject: [PATCH 09/24] lint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 872635dd..14866cff 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ expectTypeOf(good).returns.branded.inspect({ foundProps: {}, }) -expectTypeOf(good).returns.branded.inspect<{notable: 'unknown'}>({ +expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ foundProps: { '.meta.parsed': 'unknown', }, From eca25a4e8b126f5eacb35d29e102b4d62bcbc6a9 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 15:57:24 -0400 Subject: [PATCH 10/24] lint & test --- src/branding.ts | 4 ---- src/index.ts | 15 +++++++++++++-- test/__snapshots__/errors.test.ts.snap | 12 ++++++------ test/errors.test.ts | 10 ++++++---- test/types.test.ts | 16 +++++++++++----- test/usage.test.ts | 2 +- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index b4933522..d17829e5 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -115,7 +115,3 @@ export type StrictEqualUsingBranding, DeepBrand > - -type tt = DeepBrand<{d: Date}, DeepBrandOptionsDefaults> - -type tn = NominalType<{d: Date}, DeepBrandOptionsDefaults> // extends string ? [] : 1 diff --git a/src/index.ts b/src/index.ts index cd6cc44a..67bea0cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -931,13 +931,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, @@ -962,7 +969,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/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index 4a4a494c..dc40ca69 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -18,14 +18,14 @@ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does 999 expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>() ~~~~~~~~~~~~~~~~~~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: literal string: Apple, Actual: never"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. 999 expectTypeOf().toMatchTypeOf() ~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. - Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. + Property 'name' is missing in type 'Fruit' but required in type '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. 999 expectTypeOf().toEqualTypeOf() ~~~~~ @@ -33,14 +33,14 @@ test/usage.test.ts:999:999 - error TS2554: Expected 0 arguments, but got 1. 999 expectTypeOf({a: 1}).toMatchTypeOf({b: 1}) ~~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: literal string: Apple, Actual: never"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. 999 expectTypeOf().toMatchTypeOf() ~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. - Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. + Property 'name' is missing in type 'Fruit' but required in type '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. 999 expectTypeOf().toEqualTypeOf() ~~~~~ diff --git a/test/errors.test.ts b/test/errors.test.ts index f7edda65..0af71724 100644 --- a/test/errors.test.ts +++ b/test/errors.test.ts @@ -201,13 +201,15 @@ test('toEqualTypeOf with tuples', () => { }) 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 cdbe673c..201eaad7 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' import {UnionToIntersection} from '../src' import { @@ -690,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", () => { diff --git a/test/usage.test.ts b/test/usage.test.ts index 7570d5d2..c8fa5db2 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -3,7 +3,7 @@ /* eslint prettier/prettier: ["warn", { "singleQuote": true, "semi": false, "arrowParens": "avoid", "trailingComma": "es5", "bracketSpacing": false, "endOfLine": "auto", "printWidth": 100 }] */ import {test} from 'vitest' -import {DeepBrandOptionsDefaults, expectTypeOf} from '../src' +import {expectTypeOf} from '../src' test("Check an object's type with `.toEqualTypeOf`", () => { expectTypeOf({a: 1}).toEqualTypeOf<{a: number}>() From f13ec0439ef8ff1440178dc33e9ad399bb954b7a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 16:12:50 -0400 Subject: [PATCH 11/24] jsdoc --- src/utils.ts | 87 +++++++++++++++++++++++----------------------------- 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 326e91a0..aa9c633a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -230,54 +230,18 @@ export type TuplifyUnion> = */ export type UnionToTuple = TuplifyUnion -export type IsPrimitive = [T] extends [string] | [number] | [boolean] | [null] | [undefined] | [void] | [bigint] - ? true - : false - -type Entries = - IsNever extends true - ? [] - : IsNever extends true - ? [] - : UnionToTuple< - { - [K in keyof T]: [K, T[K]] - }[keyof T] - > - -export type TupleEntries = { - [K in keyof T]: [K, T[K]] -} - -export type ConcatTupleEntries = E extends [infer Head, ...infer Tail] - ? Head extends [string | number, string[]] - ? {'0': Head[1]} & ConcatTupleEntries - : {} - : {} - +/** `true` iff `T` is a tuple, as opposed to an indeterminate-length array */ export type IsTuple = number extends T['length'] ? false : true -// export type BadlyDefinedPaths = -// IsNever extends true -// ? [`${PathTo}: never`] -// : IsAny extends true -// ? [`${PathTo}: any`] -// : T extends any[] -// ? IsTuple extends true -// ? ConcatTupleEntries<{ -// [K in keyof T]: [K, BadlyDefinedPaths}`>] -// }> -// : BadlyDefinedPaths -// : Entries extends [[infer K, infer V], ...infer _Tail] -// ? BadlyDefinedPaths}`> & -// ...BadlyDefinedPaths>, PathTo>, -// ] -// : [] - -// export type BadlyDefinedDeepBrand = T extends {type: 'any' | 'never'} -// ? [`${PathTo}: ${T['type']}`] -// : never - +/** + * @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 `{gotem: true}` + * which is purely for internal use. The output should not be shown to end-users! + */ type _DeepPropTypesOfBranded = IsNever extends true ? {} @@ -287,7 +251,7 @@ type _DeepPropTypesOfBranded ? {[K in PathTo]: T['type']} & {gotem: true} : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? _DeepPropTypesOfBranded}`, FindType> & + ? _DeepPropTypesOfBranded}`, FindType> & _DeepPropTypesOfBranded>, PathTo, FindType> : {} : T extends any[] @@ -301,9 +265,24 @@ type _DeepPropTypesOfBranded }[keyof T] > +/** Required options for for {@linkcode DeepBrandPropNotes}. */ export type DeepBrandPropNotesOptions = Partial & {findType: string} +/** 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 @@ -311,14 +290,26 @@ export type DeepBrandPropNotes = : {[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' -export type PropPathSuffix = K extends 'items' +/** + * 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 = K extends 'items' ? '[number]' : K extends 'properties' ? '' : `(${Extract})` +/** The numbers between 0 and 9 that you learned in school */ export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' /** From eda1a4862326889b5246203413d1a1337c9e29fc Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 16:18:55 -0400 Subject: [PATCH 12/24] mv to branding.ts --- src/branding.ts | 79 ++++++++++++++++++++++ src/index.ts | 19 +++--- src/utils.ts | 91 ++++---------------------- test/__snapshots__/errors.test.ts.snap | 12 ++-- 4 files changed, 108 insertions(+), 93 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index d17829e5..9fe67eef 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -10,6 +10,9 @@ import { UnionToTuple, IsTuple, Not, + UnionToIntersection, + Entries, + TupleToRecord, } from './utils' export type DeepBrandOptions = { @@ -115,3 +118,79 @@ export type StrictEqualUsingBranding, 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 `{gotem: 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']} & {gotem: true} + : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` + ? Entries> extends [[infer K, infer V], ...infer _Tail] + ? _DeepPropTypesOfBranded}`, FindType> & + _DeepPropTypesOfBranded>, PathTo, FindType> + : {} + : T extends any[] + ? _DeepPropTypesOfBranded, PathTo, FindType> + : UnionToIntersection< + { + [K in keyof T]: Extract< + _DeepPropTypesOfBranded}`, FindType>, + {gotem: true} + > + }[keyof T] + > + +/** Required options for for {@linkcode DeepBrandPropNotes}. */ +export type DeepBrandPropNotesOptions = Partial & {findType: string} +/** 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 = K extends 'items' + ? '[number]' + : K extends 'properties' + ? '' + : `(${Extract})` diff --git a/src/index.ts b/src/index.ts index 67bea0cf..ee1b9dba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,11 @@ -import {DeepBrandOptions, DeepBrandOptionsDefaults, StrictEqualUsingBranding} from './branding' +import { + DeepBrandOptions, + DeepBrandOptionsDefaults, + StrictEqualUsingBranding, + DeepBrandPropNotes, + DeepBrandPropNotesOptions, + DeepBrandPropNotesOptionsDefaults, +} from './branding' import { MismatchInfo, Scolder, @@ -23,15 +30,7 @@ import { OverloadReturnTypes, OverloadsNarrowedByParameters, } from './overloads' -import { - StrictEqualUsingTSInternalIdenticalToOperator, - AValue, - MismatchArgs, - Extends, - DeepBrandPropNotes, - DeepBrandPropNotesOptions, - DeepBrandPropNotesOptionsDefaults, -} from './utils' +import {StrictEqualUsingTSInternalIdenticalToOperator, AValue, MismatchArgs, Extends} from './utils' export * from './branding' // backcompat, consider removing in next major version export * from './utils' // backcompat, consider removing in next major version diff --git a/src/utils.ts b/src/utils.ts index aa9c633a..41c56efe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,3 @@ -import {DeepBrand, DeepBrandOptions, DeepBrandOptionsDefaults as DeepBrandOptionsDefaults} from './branding' - /** * Negates a boolean type. */ @@ -230,84 +228,23 @@ export type TuplifyUnion> = */ export type UnionToTuple = TuplifyUnion -/** `true` iff `T` is a tuple, as opposed to an indeterminate-length array */ -export type IsTuple = number extends T['length'] ? false : true +export type IsPrimitive = [T] extends [string] | [number] | [boolean] | [null] | [undefined] | [void] | [bigint] + ? true + : false -/** - * @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 `{gotem: true}` - * which is purely for internal use. The output should not be shown to end-users! - */ -type _DeepPropTypesOfBranded = +export type Entries = IsNever extends true - ? {} - : T extends string - ? {} - : T extends {type: FindType} - ? {[K in PathTo]: T['type']} & {gotem: true} - : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` - ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? _DeepPropTypesOfBranded}`, FindType> & - _DeepPropTypesOfBranded>, PathTo, FindType> - : {} - : T extends any[] - ? _DeepPropTypesOfBranded, PathTo, FindType> - : UnionToIntersection< - { - [K in keyof T]: Extract< - _DeepPropTypesOfBranded}`, FindType>, - {gotem: true} - > - }[keyof T] - > - -/** Required options for for {@linkcode DeepBrandPropNotes}. */ -export type DeepBrandPropNotesOptions = Partial & {findType: string} -/** 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 + ? [] + : IsNever extends true + ? [] + : UnionToTuple< + { + [K in keyof T]: [K, T[K]] + }[keyof T] + > -/** - * @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 = K extends 'items' - ? '[number]' - : K extends 'properties' - ? '' - : `(${Extract})` +/** `true` iff `T` is a tuple, as opposed to an indeterminate-length array */ +export type IsTuple = number extends T['length'] ? false : true /** The numbers between 0 and 9 that you learned in school */ export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' diff --git a/test/__snapshots__/errors.test.ts.snap b/test/__snapshots__/errors.test.ts.snap index dc40ca69..4a4a494c 100644 --- a/test/__snapshots__/errors.test.ts.snap +++ b/test/__snapshots__/errors.test.ts.snap @@ -18,14 +18,14 @@ test/usage.test.ts:999:999 - error TS2344: Type '{ a: number; b: number; }' does 999 expectTypeOf({a: 1}).toMatchTypeOf<{a: number; b: number}>() ~~~~~~~~~~~~~~~~~~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: literal string: Apple, Actual: never"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. 999 expectTypeOf().toMatchTypeOf() ~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. - Property 'name' is missing in type 'Fruit' but required in type '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. + Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. 999 expectTypeOf().toEqualTypeOf() ~~~~~ @@ -33,14 +33,14 @@ test/usage.test.ts:999:999 - error TS2554: Expected 0 arguments, but got 1. 999 expectTypeOf({a: 1}).toMatchTypeOf({b: 1}) ~~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: literal string: Apple, Actual: never"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Apple' does not satisfy the constraint '{ name: "Expected: literal string: Apple, Actual: never"; type: "Fruit"; edible: "Expected: literal boolean: true, Actual: literal boolean: false"; }'. Types of property 'name' are incompatible. Type '"Apple"' is not assignable to type '"Expected: literal string: Apple, Actual: never"'. 999 expectTypeOf().toMatchTypeOf() ~~~~~ -test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. - Property 'name' is missing in type 'Fruit' but required in type '{ type: "Fruit"; name: "Expected: never, Actual: literal string: Apple"; edible: "Expected: boolean, Actual: never"; }'. +test/usage.test.ts:999:999 - error TS2344: Type 'Fruit' does not satisfy the constraint '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. + Property 'name' is missing in type 'Fruit' but required in type '{ name: "Expected: never, Actual: literal string: Apple"; type: "Fruit"; edible: "Expected: boolean, Actual: never"; }'. 999 expectTypeOf().toEqualTypeOf() ~~~~~ From 37f411df070941e1758970fc538aa3e7f69ee43d Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 16:29:39 -0400 Subject: [PATCH 13/24] tidy up/ rm Entries<...> --- src/branding.ts | 44 ++++++++++++++++++++------------------------ src/utils.ts | 15 --------------- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index 9fe67eef..a6026385 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -11,7 +11,6 @@ import { IsTuple, Not, UnionToIntersection, - Entries, TupleToRecord, } from './utils' @@ -125,7 +124,7 @@ export type StrictEqualUsingBrandingstring 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 `{gotem: true}` + * 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 = @@ -134,22 +133,17 @@ type _DeepPropTypesOfBranded : T extends string ? {} : T extends {type: FindType} - ? {[K in PathTo]: T['type']} & {gotem: true} - : T extends {type: string} // an object like `{type: string}` gets "branded" to `{type: 'object', properties: {type: {type: 'string'}}}` - ? Entries> extends [[infer K, infer V], ...infer _Tail] - ? _DeepPropTypesOfBranded}`, FindType> & - _DeepPropTypesOfBranded>, PathTo, FindType> - : {} - : T extends any[] - ? _DeepPropTypesOfBranded, PathTo, FindType> - : UnionToIntersection< - { - [K in keyof T]: Extract< - _DeepPropTypesOfBranded}`, FindType>, - {gotem: true} - > - }[keyof T] - > + ? {[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: string} @@ -173,7 +167,7 @@ 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]} + : {[K in Exclude]: X[K]} : never /** @@ -189,8 +183,10 @@ export type Prop = K extends string | number ? K : 'UNEXPECTED_NON_LITERAL_PR * 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 = K extends 'items' - ? '[number]' - : K extends 'properties' - ? '' - : `(${Extract})` +type DeepBrandPropPathSuffix = T extends {type: string} + ? K extends 'items' + ? '[number]' + : K extends 'properties' + ? '' + : `(${Prop})` + : `.${Prop}` diff --git a/src/utils.ts b/src/utils.ts index 41c56efe..75ef1f33 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -228,21 +228,6 @@ export type TuplifyUnion> = */ export type UnionToTuple = TuplifyUnion -export type IsPrimitive = [T] extends [string] | [number] | [boolean] | [null] | [undefined] | [void] | [bigint] - ? true - : false - -export type Entries = - IsNever extends true - ? [] - : IsNever extends true - ? [] - : UnionToTuple< - { - [K in keyof T]: [K, T[K]] - }[keyof T] - > - /** `true` iff `T` is a tuple, as opposed to an indeterminate-length array */ export type IsTuple = number extends T['length'] ? false : true From d1233dd536015fb342e03731892e96332d4152cf Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 16:34:02 -0400 Subject: [PATCH 14/24] clarify --- README.md | 6 ++++++ src/branding.ts | 4 +++- test/usage.test.ts | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14866cff..237fcf36 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,11 @@ expectTypeOf(bad).returns.branded.inspect({ '.exitCode': 'never', }, }) +``` + +You can use `.branded.inspect` to confirm there are no never/any types: +```typescript const good = (metadata: string) => ({ name: 'Bob', dob: new Date('1970-01-01'), @@ -247,6 +251,8 @@ expectTypeOf(good).returns.branded.inspect({ foundProps: {}, }) +// You can also use it to search for other types. Valid options for `findType` are currently onlly `'never' | 'any' | 'unknown'`. + expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ foundProps: { '.meta.parsed': 'unknown', diff --git a/src/branding.ts b/src/branding.ts index a6026385..f718b634 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -146,7 +146,9 @@ type _DeepPropTypesOfBranded > /** Required options for for {@linkcode DeepBrandPropNotes}. */ -export type DeepBrandPropNotesOptions = Partial & {findType: string} +export type DeepBrandPropNotesOptions = Partial & { + findType: 'any' | 'never' | 'unknown' +} /** Default options for for {@linkcode DeepBrandPropNotes}. */ export type DeepBrandPropNotesOptionsDefaults = {findType: 'any' | 'never'} diff --git a/test/usage.test.ts b/test/usage.test.ts index c8fa5db2..34d08572 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -143,7 +143,9 @@ test('Use `.inspect` to find badly-defined paths', () => { '.exitCode': 'never', }, }) +}) +test('You can use `.branded.inspect` to confirm there are no never/any types', () => { const good = (metadata: string) => ({ name: 'Bob', dob: new Date('1970-01-01'), @@ -158,6 +160,8 @@ test('Use `.inspect` to find badly-defined paths', () => { foundProps: {}, }) + // You can also use it to search for other types. Valid options for `findType` are currently onlly `'never' | 'any' | 'unknown'`. + expectTypeOf(good).returns.branded.inspect<{findType: 'unknown'}>({ foundProps: { '.meta.parsed': 'unknown', From fea841e4101001cd29a928e9166dd4a7a263ad76 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 22 Aug 2024 16:47:29 -0400 Subject: [PATCH 15/24] update title --- README.md | 2 +- test/usage.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 237fcf36..31430bd6 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ expectTypeOf<1 | null>().toBeNullable() expectTypeOf<1 | undefined | null>().toBeNullable() ``` -Use `.inspect` to find badly-defined paths: +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. diff --git a/test/usage.test.ts b/test/usage.test.ts index 34d08572..64fb72c9 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -126,7 +126,7 @@ test('Nullable types', () => { * 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. */ -test('Use `.inspect` to find badly-defined paths', () => { +test('Use `.branded.inspect` to find badly-defined paths', () => { const bad = (metadata: string) => ({ name: 'Bob', dob: new Date('1970-01-01'), From 13f164573682c4a2464b0a9b1a69e71ef284fc7b Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Sat, 24 Aug 2024 21:30:14 -0400 Subject: [PATCH 16/24] failing test --- test/types.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/types.test.ts b/test/types.test.ts index 201eaad7..0b075adf 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -907,4 +907,13 @@ 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!'}}) + + // @ts-expect-error we should be forced to say that a record has any in its RHS + expectTypeOf<{ + r: Record + }>().branded.inspect({ + foundProps: { + // '.r.foo': 'any', + }, + }) }) From a5e21e0929ccd7b13e2354a3cbdfae01dbfedafb Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:53:48 -0400 Subject: [PATCH 17/24] Update usage.test.ts --- test/usage.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/usage.test.ts b/test/usage.test.ts index 64fb72c9..d111a2f8 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -124,7 +124,10 @@ test('Nullable types', () => { /** * 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. + * 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) => ({ From a595c34205dd4569bd1749379dacd938234d4b57 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:54:29 +0000 Subject: [PATCH 18/24] [autofix.ci] apply automated fixes --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31430bd6..2e2f05f2 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,9 @@ 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, since you will get autocomplete for the bad paths, but is a fairly heavy operation, so use with caution for large/complex types. +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) => ({ From 21afbbc71103485bb4317378d702ee5c47a07ec2 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Thu, 29 Aug 2024 13:34:49 -0400 Subject: [PATCH 19/24] better record handling --- src/branding.ts | 33 ++++++++++++++++++++------------- src/utils.ts | 3 +++ test/types.test.ts | 8 ++++++-- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/branding.ts b/src/branding.ts index f718b634..a13e4d2e 100644 --- a/src/branding.ts +++ b/src/branding.ts @@ -12,6 +12,7 @@ import { Not, UnionToIntersection, TupleToRecord, + IsRecord, } from './utils' export type DeepBrandOptions = { @@ -95,20 +96,26 @@ export type DeepBrand = type: 'array' items: DeepBrand } - : { - type: 'object' - properties: { - [K in keyof T]: 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 } - 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. diff --git a/src/utils.ts b/src/utils.ts index 75ef1f33..d8de63fa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -242,3 +242,6 @@ export type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' export type TupleToRecord = { [K in keyof T as `${Extract}`]: T[K] } + +/** `true` iff `T` is a record accepting any string keys, or accepting any number keys */ +export type IsRecord = Or<[Extends, Extends]> diff --git a/test/types.test.ts b/test/types.test.ts index 0b075adf..ba25639f 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -908,12 +908,16 @@ test('inspect', () => { // 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!'}}) - // @ts-expect-error we should be forced to say that a record has any in its RHS expectTypeOf<{ r: Record }>().branded.inspect({ + // @ts-expect-error we should be forced to say that a record has any in its RHS foundProps: { - // '.r.foo': 'any', + // '.r(values)': 'any', // uncommenting this would remove the error }, }) + + expectTypeOf<{a: Record}>().branded.toEqualTypeOf<{ + a: {[K in string]: unknown + }>() }) From ad379b1ebef4cf7960fd120abaaf2cecfd267bb6 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky Date: Fri, 30 Aug 2024 10:58:47 -0400 Subject: [PATCH 20/24] } --- test/types.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types.test.ts b/test/types.test.ts index ba25639f..5b056d73 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -918,6 +918,6 @@ test('inspect', () => { }) expectTypeOf<{a: Record}>().branded.toEqualTypeOf<{ - a: {[K in string]: unknown + a: {[K in string]: unknown} }>() }) From fdfcf934dcaffce1dfd3e36921000d0d66b3d27e Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:05:57 +0000 Subject: [PATCH 21/24] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f920de1..16f125f0 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ expectTypeOf(bad).returns.branded.inspect({ }) ``` -You can use `.branded.inspect` to confirm there are no never/any types: +You can use `.branded.inspect` to confirm there are no unexpected types: ```typescript const good = (metadata: string) => ({ From 543aa93e7c24db9e1943d865334f79839ffd63a2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:06:34 +0000 Subject: [PATCH 22/24] [autofix.ci] apply automated fixes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16f125f0..0f920de1 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ expectTypeOf(bad).returns.branded.inspect({ }) ``` -You can use `.branded.inspect` to confirm there are no unexpected types: +You can use `.branded.inspect` to confirm there are no never/any types: ```typescript const good = (metadata: string) => ({ From 16b58ac3150d760927c75aba3c379cde70749e5a Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:08:22 +0000 Subject: [PATCH 23/24] Update usage.test.ts --- test/usage.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/usage.test.ts b/test/usage.test.ts index acc4afbb..7d15ea3e 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -179,7 +179,7 @@ test('Use `.branded.inspect` to find badly-defined paths', () => { }) }) -test('You can use `.branded.inspect` to confirm there are no never/any types', () => { +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'), @@ -194,7 +194,7 @@ test('You can use `.branded.inspect` to confirm there are no never/any types', ( foundProps: {}, }) - // You can also use it to search for other types. Valid options for `findType` are currently onlly `'never' | 'any' | 'unknown'`. + // 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: { From 1fa2fe82e06ca5f3898df94e6d0cb9f1c9abbc42 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 12:08:56 +0000 Subject: [PATCH 24/24] [autofix.ci] apply automated fixes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f920de1..70a21db7 100644 --- a/README.md +++ b/README.md @@ -273,7 +273,7 @@ expectTypeOf(bad).returns.branded.inspect({ }) ``` -You can use `.branded.inspect` to confirm there are no never/any types: +You can use `.branded.inspect` to confirm there are no unexpected types: ```typescript const good = (metadata: string) => ({ @@ -290,7 +290,7 @@ expectTypeOf(good).returns.branded.inspect({ foundProps: {}, }) -// You can also use it to search for other types. Valid options for `findType` are currently onlly `'never' | 'any' | 'unknown'`. +// 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: {