Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path: add bracketNotation option #926

Merged
merged 7 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> =
Emiyaaaaa marked this conversation as resolved.
Show resolved Hide resolved
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}]`);