Skip to content

Commit 7d4e875

Browse files
authored
Paths: Prevent infinite recursion (#891)
1 parent 6ef562a commit 7d4e875

File tree

3 files changed

+27
-10
lines changed

3 files changed

+27
-10
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
},
4848
"xo": {
4949
"rules": {
50+
"@typescript-eslint/no-extraneous-class": "off",
5051
"@typescript-eslint/ban-ts-comment": "off",
5152
"@typescript-eslint/ban-types": "off",
5253
"@typescript-eslint/naming-convention": "off",

source/paths.d.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ 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';
68

79
/**
810
Generate a union of all possible paths to properties in the given object.
@@ -45,22 +47,24 @@ open('listB.1'); // TypeError. Because listB only has one element.
4547
@category Object
4648
@category Array
4749
*/
48-
export type Paths<T> =
50+
export type Paths<T> = Paths_<T>;
51+
52+
type Paths_<T, Depth extends number = 0> =
4953
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
5054
? never
5155
: IsAny<T> extends true
5256
? never
5357
: T extends UnknownArray
5458
? number extends T['length']
5559
// We need to handle the fixed and non-fixed index part of the array separately.
56-
? InternalPaths<StaticPartOfArray<T>>
57-
| InternalPaths<Array<VariablePartOfArray<T>[number]>>
58-
: InternalPaths<T>
60+
? InternalPaths<StaticPartOfArray<T>, Depth>
61+
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Depth>
62+
: InternalPaths<T, Depth>
5963
: T extends object
60-
? InternalPaths<T>
64+
? InternalPaths<T, Depth>
6165
: never;
6266

63-
export type InternalPaths<_T, T = Required<_T>> =
67+
export type InternalPaths<_T, Depth extends number = 0, T = Required<_T>> =
6468
T extends EmptyObject | readonly []
6569
? never
6670
: {
@@ -71,8 +75,10 @@ export type InternalPaths<_T, T = Required<_T>> =
7175
| Key
7276
| ToString<Key>
7377
| (
74-
IsNever<Paths<T[Key]>> extends false
75-
? `${Key}.${Paths<T[Key]>}`
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
7682
: never
7783
)
7884
: never

test-d/paths.ts

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

44
declare const normal: Paths<{foo: string}>;
55
expectType<'foo'>(normal);
@@ -98,3 +98,13 @@ expectType<number | `${number}` | `${number}.b` | `${number}.a`>(leadingSpreadTu
9898

9999
declare const leadingSpreadTuple1: Paths<[...Array<{a: string}>, {b: number}, {c: number}]>;
100100
expectType<number | `${number}` | `${number}.b` | `${number}.c` | `${number}.a`>(leadingSpreadTuple1);
101+
102+
// Circularly references
103+
type MyEntity = {
104+
myOtherEntity?: MyOtherEntity;
105+
};
106+
type MyOtherEntity = {
107+
myEntity?: MyEntity;
108+
};
109+
type MyEntityPaths = Paths<MyEntity>;
110+
expectAssignable<string>({} as MyEntityPaths);

0 commit comments

Comments
 (0)