Skip to content

Commit 052e887

Browse files
fardolierisindresorhus
andauthoredJul 22, 2024··
Paths: Add maxRecursionDepth option (#920)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 8a45ba0 commit 052e887

File tree

2 files changed

+54
-28
lines changed

2 files changed

+54
-28
lines changed
 

‎source/paths.d.ts

+43-27
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,22 @@ import type {EmptyObject} from './empty-object';
33
import type {IsAny} from './is-any';
44
import type {IsNever} from './is-never';
55
import type {UnknownArray} from './unknown-array';
6-
import type {Sum} from './sum';
7-
import type {LessThan} from './less-than';
6+
import type {Subtract} from './subtract';
7+
import type {GreaterThan} from './greater-than';
8+
9+
/**
10+
Paths options.
11+
12+
@see {@link Paths}
13+
*/
14+
export type PathsOptions = {
15+
/**
16+
The maximum depth to recurse when searching for paths.
17+
18+
@default 10
19+
*/
20+
maxRecursionDepth?: number;
21+
};
822

923
/**
1024
Generate a union of all possible paths to properties in the given object.
@@ -47,39 +61,41 @@ open('listB.1'); // TypeError. Because listB only has one element.
4761
@category Object
4862
@category Array
4963
*/
50-
export type Paths<T> = Paths_<T>;
51-
52-
type Paths_<T, Depth extends number = 0> =
64+
export type Paths<T, Options extends PathsOptions = {}> =
5365
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
5466
? never
5567
: IsAny<T> extends true
5668
? never
5769
: T extends UnknownArray
5870
? number extends T['length']
5971
// We need to handle the fixed and non-fixed index part of the array separately.
60-
? InternalPaths<StaticPartOfArray<T>, Depth>
61-
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Depth>
62-
: InternalPaths<T, Depth>
72+
? InternalPaths<StaticPartOfArray<T>, Options>
73+
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Options>
74+
: InternalPaths<T, Options>
6375
: T extends object
64-
? InternalPaths<T, Depth>
76+
? InternalPaths<T, Options>
6577
: never;
6678

67-
export type InternalPaths<_T, Depth extends number = 0, T = Required<_T>> =
68-
T extends EmptyObject | readonly []
69-
? never
70-
: {
71-
[Key in keyof T]:
72-
Key extends string | number // Limit `Key` to string or number.
73-
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
74-
?
75-
| Key
76-
| ToString<Key>
77-
| (
78-
LessThan<Depth, 15> extends true // Limit the depth to prevent infinite recursion
79-
? IsNever<Paths_<T[Key], Sum<Depth, 1>>> extends false
80-
? `${Key}.${Paths_<T[Key], Sum<Depth, 1>>}`
81-
: never
79+
type InternalPaths<T, Options extends PathsOptions = {}> =
80+
(Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : 10) extends infer MaxDepth extends number
81+
? Required<T> extends infer T
82+
? T extends EmptyObject | readonly []
83+
? never
84+
: {
85+
[Key in keyof T]:
86+
Key extends string | number // Limit `Key` to string or number.
87+
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
88+
?
89+
| Key
90+
| ToString<Key>
91+
| (
92+
GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
93+
? IsNever<Paths<T[Key], {maxRecursionDepth: Subtract<MaxDepth, 1>}>> extends false
94+
? `${Key}.${Paths<T[Key], {maxRecursionDepth: Subtract<MaxDepth, 1>}>}`
95+
: never
96+
: never
97+
)
8298
: never
83-
)
84-
: never
85-
}[keyof T & (T extends UnknownArray ? number : unknown)];
99+
}[keyof T & (T extends UnknownArray ? number : unknown)]
100+
: never
101+
: never;

‎test-d/paths.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expectAssignable, expectType} from 'tsd';
2-
import type {Paths, PickDeep} from '../index';
2+
import type {Paths} from '../index';
33

44
declare const normal: Paths<{foo: string}>;
55
expectType<'foo'>(normal);
@@ -108,3 +108,13 @@ type MyOtherEntity = {
108108
};
109109
type MyEntityPaths = Paths<MyEntity>;
110110
expectAssignable<string>({} as MyEntityPaths);
111+
112+
// By default, the recursion limit should be reasonably long
113+
type RecursiveFoo = {foo: RecursiveFoo};
114+
expectAssignable<Paths<RecursiveFoo>>('foo.foo.foo.foo.foo.foo.foo.foo');
115+
116+
declare const recursion0: Paths<RecursiveFoo, {maxRecursionDepth: 0}>;
117+
expectType<'foo'>(recursion0);
118+
119+
declare const recursion1: Paths<RecursiveFoo, {maxRecursionDepth: 1}>;
120+
expectType<'foo' | 'foo.foo'>(recursion1);

0 commit comments

Comments
 (0)
Please sign in to comment.