Skip to content

Commit ac3b50e

Browse files
authored
Paths: Fix behavior with WeakMaps / WeakSets (#1348)
1 parent 7c82a21 commit ac3b50e

File tree

2 files changed

+73
-50
lines changed

2 files changed

+73
-50
lines changed

source/paths.d.ts

Lines changed: 35 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts';
1+
import type {NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions, MapsSetsOrArrays} from './internal/index.d.ts';
22
import type {IsAny} from './is-any.d.ts';
33
import type {UnknownArray} from './unknown-array.d.ts';
44
import type {GreaterThan} from './greater-than.d.ts';
@@ -192,62 +192,48 @@ open('listB.1'); // TypeError. Because listB only has one element.
192192
export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, Options>>;
193193

194194
type _Paths<T, Options extends Required<PathsOptions>, CurrentDepth extends number = 0> =
195-
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
195+
T extends NonRecursiveType | Exclude<MapsSetsOrArrays, UnknownArray>
196196
? never
197197
: IsAny<T> extends true
198198
? never
199199
: T extends object
200-
? InternalPaths<T, Options, CurrentDepth>
200+
? InternalPaths<Required<T>, Options, CurrentDepth>
201201
: never;
202202

203203
type InternalPaths<T, Options extends Required<PathsOptions>, CurrentDepth extends number> =
204-
Options['maxRecursionDepth'] extends infer MaxDepth extends number
205-
? Required<T> extends infer T
206-
? T extends readonly []
207-
? never
208-
: IsNever<keyof T> extends true // Check for empty object
209-
? never
210-
: {
211-
[Key in keyof T]:
212-
Key extends string | number // Limit `Key` to string or number.
213-
? (
214-
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
215-
? `[${Key}]`
216-
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
217-
: CurrentDepth extends 0
218-
? Key | ToString<Key>
219-
: `.${(Key | ToString<Key>)}`
220-
) extends infer TranformedKey extends string | number ?
221-
// 1. If style is 'a[0].b' and 'Key' is a numberlike value like 3 or '3', transform 'Key' to `[${Key}]`, else to `${Key}` | Key
222-
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
223-
| ((Options['leavesOnly'] extends true
224-
? MaxDepth extends CurrentDepth
225-
? TranformedKey
226-
: T[Key] extends infer Value
227-
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
228-
? TranformedKey
229-
: IsNever<keyof Value> extends true // Check for empty object
230-
? TranformedKey
231-
: never)
232-
: never
233-
: TranformedKey
234-
) extends infer _TransformedKey
235-
// If `depth` is provided, the condition becomes truthy only when it reaches `CurrentDepth`.
236-
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
237-
? CurrentDepth extends Options['depth']
238-
? _TransformedKey
239-
: never
204+
{[Key in keyof T]: Key extends string | number // Limit `Key` to `string | number`
205+
? (
206+
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
207+
? `[${Key}]`
208+
: CurrentDepth extends 0
209+
// Return both `Key` and `ToString<Key>` because for number keys, like `1`, both `1` and `'1'` are valid keys.
210+
? Key | ToString<Key>
211+
: `.${(Key | ToString<Key>)}`
212+
) extends infer TransformedKey extends string | number
213+
? ((Options['leavesOnly'] extends true
214+
? Options['maxRecursionDepth'] extends CurrentDepth
215+
? TransformedKey
216+
: T[Key] extends infer Value // For distributing `T[Key]`
217+
? (Value extends readonly [] | NonRecursiveType | Exclude<MapsSetsOrArrays, UnknownArray>
218+
? TransformedKey
219+
: IsNever<keyof Value> extends true // Check for empty object & `unknown`, because `keyof unknown` is `never`.
220+
? TransformedKey
240221
: never)
241-
| (
242-
// Recursively generate paths for the current key
243-
GreaterThan<MaxDepth, CurrentDepth> extends true // Limit the depth to prevent infinite recursion
244-
? `${TranformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
245-
: never
246-
)
247-
: never
248-
: never
249-
}[keyof T & (T extends UnknownArray ? number : unknown)]
222+
: never // Should never happen
223+
: TransformedKey
224+
) extends infer _TransformedKey
225+
// If `depth` is provided, the condition becomes truthy only when it matches `CurrentDepth`.
226+
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
227+
? CurrentDepth extends Options['depth']
228+
? _TransformedKey
229+
: never
230+
: never)
231+
// Recursively generate paths for the current key
232+
| (GreaterThan<Options['maxRecursionDepth'], CurrentDepth> extends true // Limit the depth to prevent infinite recursion
233+
? `${TransformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
234+
: never)
250235
: never
251-
: never;
236+
: never
237+
}[keyof T & (T extends UnknownArray ? number : unknown)];
252238

253239
export {};

test-d/paths.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
2-
import type {Paths} from '../index.d.ts';
2+
import type {Paths, UnknownArray} from '../index.d.ts';
3+
import type {MapsSetsOrArrays, NonRecursiveType} from '../source/internal/type.d.ts';
34

45
declare const normal: Paths<{foo: string}>;
56
expectType<'foo'>(normal);
@@ -76,6 +77,15 @@ expectType<never>(map2);
7677
declare const readonlyMap: Paths<{foo?: {bar?: ReadonlyMap<string, number>}}>;
7778
expectType<'foo' | 'foo.bar'>(readonlyMap);
7879

80+
declare const readonlyMap2: Paths<ReadonlyMap<string, number>>;
81+
expectType<never>(readonlyMap2);
82+
83+
declare const weakMap: Paths<{foo?: {bar?: WeakMap<{a: string}, number>}}>;
84+
expectType<'foo' | 'foo.bar'>(weakMap);
85+
86+
declare const weakMap2: Paths<WeakMap<{a: string}, number>>;
87+
expectType<never>(weakMap2);
88+
7989
declare const set: Paths<{foo?: {bar?: Set<string>}}>;
8090
expectType<'foo' | 'foo.bar'>(set);
8191

@@ -85,6 +95,18 @@ expectType<never>(set2);
8595
declare const readonlySet: Paths<{foo?: {bar?: ReadonlySet<string>}}>;
8696
expectType<'foo' | 'foo.bar'>(readonlySet);
8797

98+
declare const readonlySet2: Paths<ReadonlySet<string>>;
99+
expectType<never>(readonlySet2);
100+
101+
declare const weakSet: Paths<{foo?: {bar?: WeakSet<{a: string}>}}>;
102+
expectType<'foo' | 'foo.bar'>(weakSet);
103+
104+
declare const weakSet2: Paths<WeakSet<{a: string}>>;
105+
expectType<never>(weakSet2);
106+
107+
declare const nonRecursives: Paths<{a: NonRecursiveType | Exclude<MapsSetsOrArrays, UnknownArray>}>;
108+
expectType<'a'>(nonRecursives);
109+
88110
// Test for unknown length array
89111
declare const trailingSpreadTuple: Paths<[{a: string}, ...Array<{b: number}>]>;
90112
expectType<number | `${number}` | '0.a' | `${number}.b`>(trailingSpreadTuple);
@@ -183,6 +205,9 @@ expectType<'a' | 'a.c'>(unionLeaves1);
183205
declare const unionLeaves2: Paths<{a: {[x: string]: number} | {c: number}}, {leavesOnly: true}>;
184206
expectType<`a.${string}`>(unionLeaves2); // Collapsed union
185207

208+
declare const unionLeaves3: Paths<{a: string | {toLowerCase: number}}, {leavesOnly: true}>;
209+
expectType<'a' | 'a.toLowerCase'>(unionLeaves3);
210+
186211
declare const emptyObjectLeaves: Paths<{a: {}}, {leavesOnly: true}>;
187212
expectType<'a'>(emptyObjectLeaves);
188213

@@ -258,6 +283,18 @@ expectType<'a[1]' | 'a[2]'>(bracketNumericLeaves);
258283
declare const bracketNestedArrayLeaves: Paths<{a: Array<Array<Array<{b: string}>>>}, {bracketNotation: true; leavesOnly: true}>;
259284
expectType<`a[${number}][${number}][${number}].b`>(bracketNestedArrayLeaves);
260285

286+
declare const mapLeaves: Paths<{a: {b: Map<string, number>; c: ReadonlyMap<string, number>}; d: WeakMap<{a: string}, number>}, {leavesOnly: true}>;
287+
expectType<'a.b' | 'a.c' | 'd'>(mapLeaves);
288+
289+
declare const setLeaves: Paths<{a: {b: Set<string>; c: ReadonlySet<string>}; d: WeakSet<{a: string}>}, {leavesOnly: true}>;
290+
expectType<'a.b' | 'a.c' | 'd'>(setLeaves);
291+
292+
declare const unknownLeaves: Paths<{a: {b: unknown}}, {leavesOnly: true}>;
293+
expectType<'a.b'>(unknownLeaves);
294+
295+
declare const anyLeaves: Paths<{a: {b: any}}, {leavesOnly: true}>;
296+
expectType<'a.b'>(anyLeaves);
297+
261298
// -- depth option --
262299
declare const zeroDepth: Paths<DeepObject, {depth: 0}>;
263300
expectType<'a'>(zeroDepth);

0 commit comments

Comments
 (0)