Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/type-fest
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.22.1
Choose a base ref
...
head repository: sindresorhus/type-fest
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.23.0
Choose a head ref
  • 2 commits
  • 3 files changed
  • 2 contributors

Commits on Jul 22, 2024

  1. Paths: Add maxRecursionDepth option (#920)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    fardolieri and sindresorhus authored Jul 22, 2024
    Copy the full SHA
    052e887 View commit details
  2. 4.23.0

    sindresorhus committed Jul 22, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    IvanGoncharov Ivan Goncharov
    Copy the full SHA
    b9838f6 View commit details
Showing with 55 additions and 29 deletions.
  1. +1 −1 package.json
  2. +43 −27 source/paths.d.ts
  3. +11 −1 test-d/paths.ts
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "type-fest",
"version": "4.22.1",
"version": "4.23.0",
"description": "A collection of essential TypeScript types",
"license": "(MIT OR CC0-1.0)",
"repository": "sindresorhus/type-fest",
70 changes: 43 additions & 27 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
@@ -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.
@@ -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);
@@ -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);