Skip to content

Commit

Permalink
Paths: Add maxRecursionDepth option (#920)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
fardolieri and sindresorhus committed Jul 22, 2024
1 parent 8a45ba0 commit 052e887
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 28 deletions.
70 changes: 43 additions & 27 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,22 @@ import type {EmptyObject} from './empty-object';
import type {IsAny} from './is-any';
import type {IsNever} from './is-never';
import type {UnknownArray} from './unknown-array';
import type {Sum} from './sum';
import type {LessThan} from './less-than';
import type {Subtract} from './subtract';
import type {GreaterThan} from './greater-than';

/**
Paths options.
@see {@link Paths}
*/
export type PathsOptions = {
/**
The maximum depth to recurse when searching for paths.
@default 10
*/
maxRecursionDepth?: number;
};

/**
Generate a union of all possible paths to properties in the given object.
Expand Down Expand Up @@ -47,39 +61,41 @@ open('listB.1'); // TypeError. Because listB only has one element.
@category Object
@category Array
*/
export type Paths<T> = Paths_<T>;

type Paths_<T, Depth extends number = 0> =
export type Paths<T, Options extends PathsOptions = {}> =
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
? never
: IsAny<T> extends true
? never
: T extends UnknownArray
? number extends T['length']
// We need to handle the fixed and non-fixed index part of the array separately.
? InternalPaths<StaticPartOfArray<T>, Depth>
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Depth>
: InternalPaths<T, Depth>
? InternalPaths<StaticPartOfArray<T>, Options>
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Options>
: InternalPaths<T, Options>
: T extends object
? InternalPaths<T, Depth>
? InternalPaths<T, Options>
: never;

export type InternalPaths<_T, Depth extends number = 0, T = Required<_T>> =
T extends EmptyObject | readonly []
? never
: {
[Key in keyof T]:
Key extends string | number // Limit `Key` to string or number.
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
?
| Key
| ToString<Key>
| (
LessThan<Depth, 15> extends true // Limit the depth to prevent infinite recursion
? IsNever<Paths_<T[Key], Sum<Depth, 1>>> extends false
? `${Key}.${Paths_<T[Key], Sum<Depth, 1>>}`
: never
type InternalPaths<T, Options extends PathsOptions = {}> =
(Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : 10) extends infer MaxDepth extends number
? Required<T> extends infer T
? T extends EmptyObject | readonly []
? never
: {
[Key in keyof T]:
Key extends string | number // Limit `Key` to string or number.
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
?
| Key
| ToString<Key>
| (
GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
? IsNever<Paths<T[Key], {maxRecursionDepth: Subtract<MaxDepth, 1>}>> extends false
? `${Key}.${Paths<T[Key], {maxRecursionDepth: Subtract<MaxDepth, 1>}>}`
: never
: never
)
: never
)
: never
}[keyof T & (T extends UnknownArray ? number : unknown)];
}[keyof T & (T extends UnknownArray ? number : unknown)]
: never
: never;
12 changes: 11 additions & 1 deletion test-d/paths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {expectAssignable, expectType} from 'tsd';
import type {Paths, PickDeep} from '../index';
import type {Paths} from '../index';

declare const normal: Paths<{foo: string}>;
expectType<'foo'>(normal);
Expand Down Expand Up @@ -108,3 +108,13 @@ type MyOtherEntity = {
};
type MyEntityPaths = Paths<MyEntity>;
expectAssignable<string>({} as MyEntityPaths);

// By default, the recursion limit should be reasonably long
type RecursiveFoo = {foo: RecursiveFoo};
expectAssignable<Paths<RecursiveFoo>>('foo.foo.foo.foo.foo.foo.foo.foo');

declare const recursion0: Paths<RecursiveFoo, {maxRecursionDepth: 0}>;
expectType<'foo'>(recursion0);

declare const recursion1: Paths<RecursiveFoo, {maxRecursionDepth: 1}>;
expectType<'foo' | 'foo.foo'>(recursion1);

0 comments on commit 052e887

Please sign in to comment.