Skip to content

Commit

Permalink
ReadonlyDeep: Reduce likelyhood of "instantiation excessively deep"…
Browse files Browse the repository at this point in the history
… errors (#650)
  • Loading branch information
ethanresnick committed Aug 8, 2023
1 parent b2bcc38 commit cff9808
Show file tree
Hide file tree
Showing 2 changed files with 41 additions and 9 deletions.
15 changes: 12 additions & 3 deletions source/readonly-deep.d.ts
Expand Up @@ -48,9 +48,18 @@ export type ReadonlyDeep<T> = T extends BuiltIns
? ReadonlyMapDeep<KeyType, ValueType>
: T extends Readonly<ReadonlySet<infer ItemType>>
? ReadonlySetDeep<ItemType>
: T extends object
? ReadonlyObjectDeep<T>
: unknown;
: // Identify tuples to avoid converting them to arrays inadvertently; special case `readonly [...never[]]`, as it emerges undesirably from recursive invocations of ReadonlyDeep below.
T extends readonly [] | readonly [...never[]]
? readonly []
: T extends readonly [infer U, ...infer V]
? readonly [ReadonlyDeep<U>, ...ReadonlyDeep<V>]
: T extends readonly [...infer U, infer V]
? readonly [...ReadonlyDeep<U>, ReadonlyDeep<V>]
: T extends ReadonlyArray<infer ItemType>
? ReadonlyArray<ReadonlyDeep<ItemType>>
: T extends object
? ReadonlyObjectDeep<T>
: unknown;

/**
Same as `ReadonlyDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `ReadonlyDeep`.
Expand Down
35 changes: 29 additions & 6 deletions test-d/readonly-deep.ts
@@ -1,6 +1,7 @@
import {expectType, expectError} from 'tsd';
import type {ReadonlyDeep} from '../index';
import type {ReadonlyObjectDeep} from '../source/readonly-deep';
import {expectType, expectError, expectAssignable} from 'tsd';
import type {Opaque, tag} from '../source/opaque';
import type {ReadonlyDeep, ReadonlyObjectDeep} from '../source/readonly-deep';
import type {JsonValue} from '../source/basic';

type Overloaded = {
(foo: number): string;
Expand All @@ -16,6 +17,17 @@ type NamespaceWithOverload = Overloaded & {
baz: boolean[];
};

type OpaqueObjectData = {a: number[]} | {b: string};
type OpaqueObject = Opaque<OpaqueObjectData, {token: unknown}>;

type ReadonlyJsonValue =
| {readonly [k: string]: ReadonlyJsonValue}
| readonly ReadonlyJsonValue[]
| number
| string
| boolean
| null;

const data = {
object: {
foo: 'bar',
Expand All @@ -35,15 +47,20 @@ const data = {
map: new Map<string, string>(),
set: new Set<string>(),
array: ['foo'],
tuple: ['foo'] as ['foo'],
emptyTuple: [] as [],
singleItemTuple: ['foo'] as ['foo'],
multiItemTuple: [{a: ''}, {b: 4}, {c: ''}] as [{a: string}, {b: number}, {c: string}],
trailingSpreadTuple: ['foo', 1] as [string, ...number[]],
leadingSpreadTuple: ['foo', 1] as [...string[], number],
readonlyMap: new Map<string, string>() as ReadonlyMap<string, string>,
readonlySet: new Set<string>() as ReadonlySet<string>,
readonlyArray: ['foo'] as readonly string[],
readonlyTuple: ['foo'] as const,
json: [{x: true}] as JsonValue,
opaqueObj: {a: [3]} as OpaqueObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions
};

const readonlyData: ReadonlyDeep<typeof data> = data;

readonlyData.fn('foo');

readonlyData.fnWithOverload(1);
Expand All @@ -62,11 +79,17 @@ expectType<RegExp>(readonlyData.regExp);
expectType<Readonly<ReadonlyMap<string, string>>>(readonlyData.map);
expectType<Readonly<ReadonlySet<string>>>(readonlyData.set);
expectType<readonly string[]>(readonlyData.array);
expectType<readonly ['foo']>(readonlyData.tuple);
expectType<readonly []>(readonlyData.emptyTuple);
expectType<readonly ['foo']>(readonlyData.singleItemTuple);
expectType<readonly [string, ...number[]]>(readonlyData.trailingSpreadTuple);
expectType<readonly [...string[], number]>(readonlyData.leadingSpreadTuple);
expectType<readonly [{readonly a: string}, {readonly b: number}, {readonly c: string}]>(readonlyData.multiItemTuple);
expectType<Readonly<ReadonlyMap<string, string>>>(readonlyData.readonlyMap);
expectType<Readonly<ReadonlySet<string>>>(readonlyData.readonlySet);
expectType<readonly string[]>(readonlyData.readonlyArray);
expectType<readonly ['foo']>(readonlyData.readonlyTuple);
expectAssignable<ReadonlyJsonValue>(readonlyData.json);
expectAssignable<Opaque<ReadonlyDeep<OpaqueObjectData>, ReadonlyDeep<OpaqueObject[typeof tag]>>>(readonlyData.opaqueObj);

expectType<((foo: number) => string) & ReadonlyObjectDeep<Namespace>>(readonlyData.namespace);
expectType<string>(readonlyData.namespace(1));
Expand Down

0 comments on commit cff9808

Please sign in to comment.