Skip to content

Commit

Permalink
Paths: Add depth option (#1058)
Browse files Browse the repository at this point in the history
  • Loading branch information
som-sm authored Feb 16, 2025
1 parent c8149ec commit 2633e5b
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 7 deletions.
63 changes: 60 additions & 3 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,48 @@ export type PathsOptions = {
```
*/
leavesOnly?: boolean;

/**
Only include paths at the specified depth. By default all paths up to {@link PathsOptions.maxRecursionDepth | `maxRecursionDepth`} are included.
Note: Depth starts at `0` for root properties.
@default number
@example
```
type Post = {
id: number;
author: {
id: number;
name: {
first: string;
last: string;
};
};
};
type DepthZero = Paths<Post, {depth: 0}>;
//=> 'id' | 'author'
type DepthOne = Paths<Post, {depth: 1}>;
//=> 'author.id' | 'author.name'
type DepthTwo = Paths<Post, {depth: 2}>;
//=> 'author.name.first' | 'author.name.last'
type LeavesAtDepthOne = Paths<Post, {leavesOnly: true; depth: 1}>;
//=> 'author.id'
```
*/
depth?: number;
};

type DefaultPathsOptions = {
maxRecursionDepth: 10;
bracketNotation: false;
leavesOnly: false;
depth: number;
};

/**
Expand Down Expand Up @@ -147,6 +183,8 @@ export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, {
bracketNotation: Options['bracketNotation'] extends boolean ? Options['bracketNotation'] : DefaultPathsOptions['bracketNotation'];
// Set default leavesOnly to false
leavesOnly: Options['leavesOnly'] extends boolean ? Options['leavesOnly'] : DefaultPathsOptions['leavesOnly'];
// Set default depth to number
depth: Options['depth'] extends number ? Options['depth'] : DefaultPathsOptions['depth'];
}>;

type _Paths<T, Options extends Required<PathsOptions>> =
Expand Down Expand Up @@ -186,17 +224,36 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
) extends infer TranformedKey extends string | number ?
// 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
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
| (Options['leavesOnly'] extends true
| ((Options['leavesOnly'] extends true
? MaxDepth extends 0
? TranformedKey
: T[Key] extends EmptyObject | readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
? TranformedKey
: never
: TranformedKey)
: TranformedKey
) extends infer _TransformedKey
// If `depth` is provided, the condition becomes truthy only when it reaches `0`.
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
? 0 extends Options['depth']
? _TransformedKey
: never
: never)
| (
// Recursively generate paths for the current key
GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
? _Paths<T[Key], {bracketNotation: Options['bracketNotation']; maxRecursionDepth: Subtract<MaxDepth, 1>; leavesOnly: Options['leavesOnly']}> extends infer SubPath
? _Paths<T[Key],
{
bracketNotation: Options['bracketNotation'];
maxRecursionDepth: Subtract<MaxDepth, 1>;
leavesOnly: Options['leavesOnly'];
depth: Options['depth'] extends infer Depth extends number // For distributing `Options['depth']`
? Depth extends 0 // Don't subtract further if `Depth` has reached `0`
? never
: ToString<Depth> extends `-${number}` // Don't subtract if `Depth` is -ve
? never
: Subtract<Options['depth'], 1> // If `Subtract` supported -ve numbers, then `depth` could have simply been `Subtract<Options['depth'], 1>`
: never; // Should never happen
}> extends infer SubPath
? SubPath extends string | number
? (
Options['bracketNotation'] extends true
Expand Down
166 changes: 162 additions & 4 deletions test-d/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ expectType<`${number}.a` | `${number}.b` | `${number}.c`>(leadingSpreadLeaves1);
declare const recursiveLeaves: Paths<RecursiveFoo, {leavesOnly: true}>;
expectType<'foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo'>(recursiveLeaves);

declare const recursiveWithDepthLeaves: Paths<RecursiveFoo, {maxRecursionDepth: 0; leavesOnly: true}>;
expectType<'foo'>(recursiveWithDepthLeaves);
declare const recursiveWithMaxLeaves: Paths<RecursiveFoo, {maxRecursionDepth: 0; leavesOnly: true}>;
expectType<'foo'>(recursiveWithMaxLeaves);

declare const recursiveWithDepthLeaves1: Paths<RecursiveFoo, {maxRecursionDepth: 1; leavesOnly: true}>;
expectType<'foo.foo'>(recursiveWithDepthLeaves1);
declare const recursiveWithMaxLeaves1: Paths<RecursiveFoo, {maxRecursionDepth: 1; leavesOnly: true}>;
expectType<'foo.foo'>(recursiveWithMaxLeaves1);

declare const recursiveArrayLeaves: Paths<RecursionArray, {bracketNotation: true; maxRecursionDepth: 2; leavesOnly: true}>;
expectType<`[${number}][${number}][${number}]`>(recursiveArrayLeaves);
Expand All @@ -229,3 +229,161 @@ expectType<'a[1]' | 'a[2]'>(bracketNumericLeaves);

declare const bracketNestedArrayLeaves: Paths<{a: Array<Array<Array<{b: string}>>>}, {bracketNotation: true; leavesOnly: true}>;
expectType<`a[${number}][${number}][${number}].b`>(bracketNestedArrayLeaves);

// -- depth option --
declare const zeroDepth: Paths<DeepObject, {depth: 0}>;
expectType<'a'>(zeroDepth);

declare const oneDepth: Paths<DeepObject, {depth: 1}>;
expectType<'a.b' | 'a.b2' | 'a.b3'>(oneDepth);

declare const twoDepth: Paths<DeepObject, {depth: 2}>;
expectType<'a.b.c' | `a.b2.${number}`>(twoDepth);

declare const threeDepth: Paths<DeepObject, {depth: 3}>;
expectType<'a.b.c.d'>(threeDepth);

declare const unionDepth: Paths<{a: {readonly b?: string}} | {x: {y?: {z: number}}}, {depth: 1}>;
expectType<'a.b' | 'x.y'>(unionDepth);

declare const unionDepth2: Paths<{a?: {b: string; readonly c: {d?: string}}} | {readonly x?: {y: string}}, {depth: 2}>;
expectType<'a.c.d'>(unionDepth2);

declare const unionDepth3: Paths<DeepObject, {depth: 0 | 3}>;
expectType<'a' | 'a.b.c.d'>(unionDepth3);

declare const unionDepth4: Paths<{a?: {b: string; readonly c: {d?: [string, number]}}} | {readonly x?: {y: string}}, {depth: 1 | 3}>;
expectType<'a.b' | 'a.c' | 'x.y' | 'a.c.d.0' | 'a.c.d.1'>(unionDepth4);

declare const unionDepth5: Paths<{a: {b?: string}} | {x: {y?: {z: number}}}, {depth: 0 | 2}>;
expectType<'a' | 'x' | 'x.y.z'>(unionDepth5);

declare const unreachableDepth: Paths<DeepObject, {depth: 4}>;
expectType<never>(unreachableDepth);

declare const unreachableDepth2: Paths<{a: {}}, {depth: 1}>;
expectType<never>(unreachableDepth2);

declare const unreachableDepth3: Paths<{a: readonly []}, {depth: 1}>;
expectType<never>(unreachableDepth3);

declare const unreachableAndReachableDepth: Paths<{a: {b: string}}, {depth: 1 | 2}>;
expectType<'a.b'>(unreachableAndReachableDepth);

declare const maxLessThanDepth: Paths<DeepObject, {maxRecursionDepth: 2; depth: 3}>;
expectType<never>(maxLessThanDepth);

declare const maxLessThanDepth2: Paths<RecursiveFoo, {depth: 12}>; // Default `maxRecursionDepth` is 10
expectType<never>(maxLessThanDepth2);

declare const maxSimilarToDepth: Paths<DeepObject, {maxRecursionDepth: 2; depth: 2}>;
expectType<'a.b.c' | `a.b2.${number}`>(maxSimilarToDepth);

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

declare const maxSimilarToDepth3: Paths<RecursiveFoo, {depth: 10}>; // Default `maxRecursionDepth` is 10
expectType<'foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo'>(maxSimilarToDepth3);

declare const maxMoreThanDepth: Paths<DeepObject, {maxRecursionDepth: 2; depth: 1}>;
expectType<'a.b' | 'a.b2' | 'a.b3'>(maxMoreThanDepth);

declare const maxMoreThanDepth2: Paths<RecursiveFoo, {maxRecursionDepth: 6; depth: 3}>;
expectType<'foo.foo.foo.foo'>(maxMoreThanDepth2);

declare const maxMoreAndLessThanDepth: Paths<RecursionArray, {maxRecursionDepth: 2; depth: 1 | 3}>;
expectType<`${number}.${number}`>(maxMoreAndLessThanDepth);

declare const maxLessAndSimilarThanDepth: Paths<RecursionArray, {maxRecursionDepth: 1; depth: 0 | 1}>;
expectType<number | `${number}` | `${number}.${number}`>(maxLessAndSimilarThanDepth);

declare const maxSimilarAndMoreThanDepth: Paths<RecursionArray, {maxRecursionDepth: 2; depth: 2 | 3}>;
expectType<`${number}.${number}.${number}`>(maxSimilarAndMoreThanDepth);

declare const noLeavesAtDepth: Paths<DeepObject, {leavesOnly: true; depth: 0}>;
expectType<never>(noLeavesAtDepth);

declare const onlyLeavesAtDepth: Paths<DeepObject, {leavesOnly: true; depth: 1}>;
expectType<'a.b3'>(onlyLeavesAtDepth);

declare const onlyLeavesAtDepth2: Paths<DeepObject, {leavesOnly: true; depth: 2}>;
expectType<`a.b2.${number}`>(onlyLeavesAtDepth2);

declare const deepArrayDepth: Paths<{a?: {b: readonly string[]}; c: boolean[]}, {depth: 0 | 2}>;
expectType<'a' | 'c' | `a.b.${number}`>(deepArrayDepth);

declare const deepTupleDepth: Paths<{a: {b: [string, number]}}, {depth: 2}>;
expectType<'a.b.0' | 'a.b.1'>(deepTupleDepth);

declare const deepObjectArrayDepth: Paths<{a: {b: ReadonlyArray<{readonly c?: number; d: string}>}}, {depth: 1 | 3}>;
expectType<'a.b' | `a.b.${number}.c` | `a.b.${number}.d`>(deepObjectArrayDepth);

declare const deepObjectTupleDepth: Paths<{a: {readonly b: [{readonly c: string}, {d?: [number]}]}}, {leavesOnly: true; depth: 3}>;
expectType<'a.b.0.c'>(deepObjectTupleDepth);

declare const nestedArrayDepth: Paths<{a?: Array<Array<Array<{b: string}>>>}, {depth: 1 | 2 | 3}>;
expectType<`a.${number}` | `a.${number}.${number}` | `a.${number}.${number}.${number}`>(nestedArrayDepth);

declare const nestedTupleDepth: Paths<{a: [[[{b: string}]]?]}, {depth: 0 | 4}>;
expectType<'a' | 'a.0.0.0.b'>(nestedTupleDepth);

declare const recursiveDepth: Paths<RecursiveFoo, {depth: 4}>;
expectType<'foo.foo.foo.foo.foo'>(recursiveDepth);

declare const recursiveDepth2: Paths<RecursiveFoo, {depth: 1 | 3 | 8}>;
expectType<'foo.foo' | 'foo.foo.foo.foo' | 'foo.foo.foo.foo.foo.foo.foo.foo.foo'>(recursiveDepth2);

// For recursive types, leaves are at `maxRecursionDepth`
declare const recursiveDepth3: Paths<RecursiveFoo, {leavesOnly: true; depth: 5}>;
expectType<never>(recursiveDepth3);

declare const recursiveDepth4: Paths<RecursiveFoo, {leavesOnly: true; depth: 5 | 10}>; // No leaves at depth `5`
expectType<'foo.foo.foo.foo.foo.foo.foo.foo.foo.foo.foo'>(recursiveDepth4);

declare const recursiveDepth6: Paths<RecursiveFoo, {leavesOnly: true; maxRecursionDepth: 6; depth: 5}>; // Leaves are at depth `6`
expectType<never>(recursiveDepth6);

declare const maxLeavesAndDepth: Paths<DeepObject, {leavesOnly: true; maxRecursionDepth: 2; depth: 2}>; // All depth `2` paths are leaves
expectType<'a.b.c' | `a.b2.${number}`>(maxLeavesAndDepth);

declare const maxLeavesAndDepth2: Paths<DeepObject, {leavesOnly: true; maxRecursionDepth: 2; depth: 0 | 1 | 2}>;
expectType<'a.b3' | 'a.b.c' | `a.b2.${number}`>(maxLeavesAndDepth2);

declare const recursiveBracketDepth: Paths<RecursionArray, {bracketNotation: true; depth: 3}>;
expectType<`[${number}][${number}][${number}][${number}]`>(recursiveBracketDepth);

declare const recursiveBracketDepth2: Paths<RecursionArray, {bracketNotation: true; leavesOnly: true; depth: 3}>; // Leaves are at depth `10`
expectType<never>(recursiveBracketDepth2);

declare const bracketArrayDepth: Paths<{a: Array<{b: string; c?: string}>}, {bracketNotation: true; depth: 1 | 2}>;
expectType<`a[${number}]` | `a[${number}].b` | `a[${number}].c`>(bracketArrayDepth);

declare const bracketTupleDepth: Paths<{a: [{b?: string}, {c: string}]}, {bracketNotation: true; leavesOnly: true; depth: 0 | 2}>;
expectType<'a[0].b' | 'a[1].c'>(bracketTupleDepth);

declare const bracketNumericDepth: Paths<{a: {1: string; 2: number}}, {bracketNotation: true; depth: 1}>;
expectType<'a[1]' | 'a[2]'>(bracketNumericDepth);

declare const bracketNestedArrayDepth: Paths<{a: Array<Array<Array<{b: string}>>>}, {bracketNotation: true; depth: 2 | 4}>;
expectType<`a[${number}][${number}]` | `a[${number}][${number}][${number}].b`>(bracketNestedArrayDepth);

declare const trailingSpreadDepth: Paths<[{a: string}, ...Array<{b: number}>], {depth: 1}>;
expectType<'0.a' | `${number}.b`>(trailingSpreadDepth);

declare const leadingSpreadDepth: Paths<[...Array<{a?: string}>, {readonly b: number}], {depth: 0 | 1}>;
expectType<number | `${number}` | `${number}.b` | `${number}.a`>(leadingSpreadDepth);

declare const negativeDepth: Paths<DeepObject, {depth: -1}>;
expectType<never>(negativeDepth);

declare const positiveAndNegativeDepth: Paths<DeepObject, {depth: 1 | -1}>;
expectType<'a.b' | 'a.b2' | 'a.b3'>(positiveAndNegativeDepth);

declare const zeroPositiveAndNegativeDepth: Paths<DeepObject, {depth: 0 | 2 | -4}>;
expectType<'a' | 'a.b.c' | `a.b2.${number}`>(zeroPositiveAndNegativeDepth);

declare const neverDepth: Paths<DeepObject, {depth: never}>;
expectType<never>(neverDepth);

declare const anyDepth: Paths<DeepObject, {depth: any}>;
expectType<'a' | 'a.b.c' | `a.b2.${number}` | 'a.b3' | 'a.b' | 'a.b2' | 'a.b.c.d'>(anyDepth);

0 comments on commit 2633e5b

Please sign in to comment.