Skip to content

Commit 87d413e

Browse files
committed
fix: DeepUnbranded type: later bailout by compiler
1 parent 62a3081 commit 87d413e

File tree

2 files changed

+42
-3
lines changed

2 files changed

+42
-3
lines changed

src/interfaces.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { expectTypeOf } from 'expect-type';
2+
import { DeepUnbranded, Unbranded, WithBrands } from './interfaces';
3+
4+
test('DeepUnbranded', () => {
5+
type BrandedString = WithBrands<string, 'BrandedString'>;
6+
expectTypeOf<DeepUnbranded<BrandedString>>().toEqualTypeOf<string>();
7+
8+
type BrandedObject = WithBrands<{ string: BrandedString }, 'BrandedObject'>;
9+
expectTypeOf<Unbranded<BrandedObject>>().toEqualTypeOf<{ string: BrandedString }>();
10+
expectTypeOf<Unbranded<BrandedObject>>().not.toEqualTypeOf<{ string: string }>();
11+
expectTypeOf<DeepUnbranded<BrandedObject>>().toEqualTypeOf<{ string: string }>();
12+
13+
type BrandedArray = WithBrands<BrandedObject[], 'BrandedArray'>;
14+
expectTypeOf<Unbranded<BrandedArray>>().toEqualTypeOf<BrandedObject[]>();
15+
expectTypeOf<DeepUnbranded<BrandedArray>>().toEqualTypeOf<Array<{ string: string }>>();
16+
17+
type BrandedReadonlyArray = WithBrands<readonly BrandedObject[], 'BrandedReadonlyArray'>;
18+
expectTypeOf<Unbranded<BrandedReadonlyArray>>().toEqualTypeOf<readonly BrandedObject[]>();
19+
expectTypeOf<DeepUnbranded<BrandedReadonlyArray>>().toEqualTypeOf<ReadonlyArray<{ string: string }>>();
20+
21+
type BrandedEmptyTuple = WithBrands<[], 'BrandedEmptyTuple'>;
22+
expectTypeOf<Unbranded<BrandedEmptyTuple>>().toEqualTypeOf<[]>();
23+
expectTypeOf<DeepUnbranded<BrandedEmptyTuple>>().toEqualTypeOf<[]>();
24+
25+
type BrandedTuple = WithBrands<readonly [BrandedObject, BrandedObject, BrandedObject], 'BrandedTuple'>;
26+
expectTypeOf<Unbranded<BrandedTuple>>().toEqualTypeOf<readonly [BrandedObject, BrandedObject, BrandedObject]>();
27+
expectTypeOf<DeepUnbranded<BrandedTuple>>().toHaveProperty('length').toEqualTypeOf<3>();
28+
// This is the best we can do for now: (note the `toMatchTypeOf` instead of the `toEqualTypeOf`)
29+
expectTypeOf<DeepUnbranded<BrandedTuple>>().toMatchTypeOf<readonly [{ string: string }, { string: string }, { string: string }]>();
30+
31+
// It should be enough for most cases:
32+
[{ string: 'abc' }, { string: 'abc' }, { string: 'abc' }] satisfies DeepUnbranded<BrandedTuple>;
33+
});

src/interfaces.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -214,12 +214,18 @@ export type WithBrands<T, BrandNames extends string> = T & { readonly [brands]:
214214
export type Unbranded<T> = T extends WithBrands<infer Base, any> ? Base : T;
215215

216216
/** Unbrand a given type (recursive). */
217-
export type DeepUnbranded<T> = T extends ReadonlyArray<unknown>
218-
? { [P in keyof T & number]: DeepUnbranded<T[P]> }
217+
export type DeepUnbranded<T> = T extends readonly [any, ...any[]] | readonly []
218+
? UnbrandValues<Unbranded<T>>
219+
: T extends Array<infer E>
220+
? Array<DeepUnbranded<E>>
221+
: T extends ReadonlyArray<infer E>
222+
? ReadonlyArray<DeepUnbranded<E>>
219223
: T extends Record<string, unknown>
220-
? Omit<{ [P in keyof T]: DeepUnbranded<T[P]> }, typeof brands>
224+
? UnbrandValues<Unbranded<T>>
221225
: Unbranded<T>;
222226

227+
export type UnbrandValues<T> = { [P in keyof T]: DeepUnbranded<T[P]> };
228+
223229
/**
224230
* The properties of an object type.
225231
*

0 commit comments

Comments
 (0)