Skip to content

Commit

Permalink
Path: Add bracketNotation option (#926)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Emiyaaaaa and sindresorhus authored Aug 8, 2024
1 parent f361912 commit 3b15a94
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 17 deletions.
27 changes: 27 additions & 0 deletions source/internal/numeric.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,33 @@ NumberAbsolute<NegativeInfinity>
*/
export type NumberAbsolute<N extends number> = `${N}` extends `-${infer StringPositiveN}` ? StringToNumber<StringPositiveN> : N;

/**
Check whether the given type is a number or a number string.
Supports floating-point as a string.
@example
```
type A = IsNumberLike<'1'>;
//=> true
type B = IsNumberLike<'-1.1'>;
//=> true
type C = IsNumberLike<1>;
//=> true
type D = IsNumberLike<'a'>;
//=> false
*/
export type IsNumberLike<N> =
N extends number ? true
: N extends `${number}`
? true
: N extends `${number}.${number}`
? true
: false;

/**
Returns the minimum number in the given union of numbers.
Expand Down
99 changes: 84 additions & 15 deletions source/paths.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString} from './internal';
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike} from './internal';
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 {Subtract} from './subtract';
import type {GreaterThan} from './greater-than';
Expand All @@ -18,6 +17,44 @@ export type PathsOptions = {
@default 10
*/
maxRecursionDepth?: number;

/**
Use bracket notation for array indices and numeric object keys.
@default false
@example
```
type ArrayExample = {
array: ['foo'];
};
type A = Paths<ArrayExample, {bracketNotation: false}>;
//=> 'array' | 'array.0'
type B = Paths<ArrayExample, {bracketNotation: true}>;
//=> 'array' | 'array[0]'
```
@example
```
type NumberKeyExample = {
1: ['foo'];
};
type A = Paths<NumberKeyExample, {bracketNotation: false}>;
//=> 1 | '1' | '1.0'
type B = Paths<NumberKeyExample, {bracketNotation: true}>;
//=> '[1]' | '[1][0]'
```
*/
bracketNotation?: boolean;
};

type DefaultPathsOptions = {
maxRecursionDepth: 10;
bracketNotation: false;
};

/**
Expand Down Expand Up @@ -61,7 +98,14 @@ open('listB.1'); // TypeError. Because listB only has one element.
@category Object
@category Array
*/
export type Paths<T, Options extends PathsOptions = {}> =
export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, {
// Set default maxRecursionDepth to 10
maxRecursionDepth: Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : DefaultPathsOptions['maxRecursionDepth'];
// Set default bracketNotation to false
bracketNotation: Options['bracketNotation'] extends boolean ? Options['bracketNotation'] : DefaultPathsOptions['bracketNotation'];
}>;

type _Paths<T, Options extends Required<PathsOptions>> =
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
? never
: IsAny<T> extends true
Expand All @@ -76,25 +120,50 @@ export type Paths<T, Options extends PathsOptions = {}> =
? InternalPaths<T, Options>
: never;

type InternalPaths<T, Options extends PathsOptions = {}> =
(Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : 10) extends infer MaxDepth extends number
type InternalPaths<T, Options extends Required<PathsOptions>> =
Options['maxRecursionDepth'] 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
? (
Options['bracketNotation'] extends true
? IsNumberLike<Key> extends true
? `[${Key}]`
: (Key | ToString<Key>)
: never
)
|
Options['bracketNotation'] extends false
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
? (Key | ToString<Key>)
: never
) 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
| TranformedKey
| (
// 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>}> extends infer SubPath
? SubPath extends string | number
? (
Options['bracketNotation'] extends true
? SubPath extends `[${any}]` | `[${any}]${string}`
? `${TranformedKey}${SubPath}` // If next node is number key like `[3]`, no need to add `.` before it.
: `${TranformedKey}.${SubPath}`
: never
) | (
Options['bracketNotation'] extends false
? `${TranformedKey}.${SubPath}`
: never
)
: never
: never
: never
)
: never
: never
}[keyof T & (T extends UnknownArray ? number : unknown)]
: never
Expand Down
1 change: 0 additions & 1 deletion test-d/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ type WithModifiers = {

expectTypeOf<Get<WithModifiers, 'foo[0].bar.baz', NonStrict>>().toEqualTypeOf<{qux: number} | undefined>();
expectTypeOf<Get<WithModifiers, 'foo[0].abc.def.ghi', NonStrict>>().toEqualTypeOf<string | undefined>();

// Test bracket notation
expectTypeOf<Get<number[], '[0]', NonStrict>>().toBeNumber();
// NOTE: This would fail if `[0][0]` was converted into `00`:
Expand Down
8 changes: 8 additions & 0 deletions test-d/internal/is-number-like.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {expectType} from 'tsd';
import type {IsNumberLike} from '../../source/internal/numeric.d';

expectType<IsNumberLike<'1'>>(true);
expectType<IsNumberLike<1>>(true);
expectType<IsNumberLike<'-1.1'>>(true);
expectType<IsNumberLike< -1.1>>(true);
expectType<IsNumberLike<'foo'>>(false);
30 changes: 29 additions & 1 deletion test-d/paths.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {expectAssignable, expectType} from 'tsd';
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
import type {Paths} from '../index';

declare const normal: Paths<{foo: string}>;
Expand Down Expand Up @@ -118,3 +118,31 @@ expectType<'foo'>(recursion0);

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

// Test a[0].b style
type Object1 = {
arr: [{a: string}];
};
expectType<Paths<Object1, {bracketNotation: true}>>({} as 'arr' | 'arr[0]' | 'arr[0].a');

type Object2 = {
arr: Array<{a: string}>;
arr1: string[];
};
expectType<Paths<Object2, {bracketNotation: true}>>({} as 'arr' | 'arr1' | `arr[${number}]` | `arr[${number}].a` | `arr1[${number}]`);

type Object3 = {
1: 'foo';
'2': 'bar';
};
expectType<Paths<Object3, {bracketNotation: true}>>({} as '[1]' | '[2]');

type deepArray = {
arr: Array<Array<Array<{a: string}>>>;
};
expectType<Paths<deepArray, {bracketNotation: true}>>({} as 'arr' | `arr[${number}]` | `arr[${number}][${number}]` | `arr[${number}][${number}][${number}]` | `arr[${number}][${number}][${number}].a`);

type RecursionArray = RecursionArray[];
type RecursionArrayPaths = Paths<RecursionArray, {bracketNotation: true; maxRecursionDepth: 3}>;
expectAssignable<RecursionArrayPaths>({} as `[${number}][${number}][${number}][${number}]`);
expectNotAssignable<RecursionArrayPaths>({} as `[${number}][${number}][${number}][${number}][${number}]`);

0 comments on commit 3b15a94

Please sign in to comment.