From cff9808189acce7622e4c84a7129b1d6888f0909 Mon Sep 17 00:00:00 2001 From: Ethan Resnick Date: Tue, 8 Aug 2023 05:46:49 -0400 Subject: [PATCH] `ReadonlyDeep`: Reduce likelyhood of "instantiation excessively deep" errors (#650) --- source/readonly-deep.d.ts | 15 ++++++++++++--- test-d/readonly-deep.ts | 35 +++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/source/readonly-deep.d.ts b/source/readonly-deep.d.ts index a4f0c728d..fbfec11f0 100644 --- a/source/readonly-deep.d.ts +++ b/source/readonly-deep.d.ts @@ -48,9 +48,18 @@ export type ReadonlyDeep = T extends BuiltIns ? ReadonlyMapDeep : T extends Readonly> ? ReadonlySetDeep - : T extends object - ? ReadonlyObjectDeep - : 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, ...ReadonlyDeep] + : T extends readonly [...infer U, infer V] + ? readonly [...ReadonlyDeep, ReadonlyDeep] + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends object + ? ReadonlyObjectDeep + : unknown; /** Same as `ReadonlyDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `ReadonlyDeep`. diff --git a/test-d/readonly-deep.ts b/test-d/readonly-deep.ts index 9a8793d3f..bb7f677a9 100644 --- a/test-d/readonly-deep.ts +++ b/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; @@ -16,6 +17,17 @@ type NamespaceWithOverload = Overloaded & { baz: boolean[]; }; +type OpaqueObjectData = {a: number[]} | {b: string}; +type OpaqueObject = Opaque; + +type ReadonlyJsonValue = + | {readonly [k: string]: ReadonlyJsonValue} + | readonly ReadonlyJsonValue[] + | number + | string + | boolean + | null; + const data = { object: { foo: 'bar', @@ -35,15 +47,20 @@ const data = { map: new Map(), set: new Set(), 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() as ReadonlyMap, readonlySet: new Set() as ReadonlySet, 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 = data; - readonlyData.fn('foo'); readonlyData.fnWithOverload(1); @@ -62,11 +79,17 @@ expectType(readonlyData.regExp); expectType>>(readonlyData.map); expectType>>(readonlyData.set); expectType(readonlyData.array); -expectType(readonlyData.tuple); +expectType(readonlyData.emptyTuple); +expectType(readonlyData.singleItemTuple); +expectType(readonlyData.trailingSpreadTuple); +expectType(readonlyData.leadingSpreadTuple); +expectType(readonlyData.multiItemTuple); expectType>>(readonlyData.readonlyMap); expectType>>(readonlyData.readonlySet); expectType(readonlyData.readonlyArray); expectType(readonlyData.readonlyTuple); +expectAssignable(readonlyData.json); +expectAssignable, ReadonlyDeep>>(readonlyData.opaqueObj); expectType<((foo: number) => string) & ReadonlyObjectDeep>(readonlyData.namespace); expectType(readonlyData.namespace(1));