diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 089120fb5..b2e14882b 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -19,6 +19,33 @@ NumberAbsolute */ export type NumberAbsolute = `${N}` extends `-${infer StringPositiveN}` ? StringToNumber : 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 extends number ? true + : N extends `${number}` + ? true + : N extends `${number}.${number}` + ? true + : false; + /** Returns the minimum number in the given union of numbers. diff --git a/source/paths.d.ts b/source/paths.d.ts index 3d4a232a3..7e9f36599 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -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'; @@ -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; + //=> 'array' | 'array.0' + + type B = Paths; + //=> 'array' | 'array[0]' + ``` + + @example + ``` + type NumberKeyExample = { + 1: ['foo']; + }; + + type A = Paths; + //=> 1 | '1' | '1.0' + + type B = Paths; + //=> '[1]' | '[1][0]' + ``` + */ + bracketNotation?: boolean; +}; + +type DefaultPathsOptions = { + maxRecursionDepth: 10; + bracketNotation: false; }; /** @@ -61,7 +98,14 @@ open('listB.1'); // TypeError. Because listB only has one element. @category Object @category Array */ -export type Paths = +export type Paths = _Paths; + +type _Paths> = T extends NonRecursiveType | ReadonlyMap | ReadonlySet ? never : IsAny extends true @@ -76,25 +120,50 @@ export type Paths = ? InternalPaths : never; -type InternalPaths = - (Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : 10) extends infer MaxDepth extends number +type InternalPaths> = + Options['maxRecursionDepth'] extends infer MaxDepth extends number ? Required 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 - | ( - GreaterThan extends true // Limit the depth to prevent infinite recursion - ? IsNever}>> extends false - ? `${Key}.${Paths}>}` - : never + ? ( + Options['bracketNotation'] extends true + ? IsNumberLike extends true + ? `[${Key}]` + : (Key | ToString) : never - ) + | + Options['bracketNotation'] extends false + // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work. + ? (Key | ToString) + : 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 extends true // Limit the depth to prevent infinite recursion + ? _Paths}> 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 diff --git a/test-d/get.ts b/test-d/get.ts index 2d62f1720..640a397e0 100644 --- a/test-d/get.ts +++ b/test-d/get.ts @@ -109,7 +109,6 @@ type WithModifiers = { expectTypeOf>().toEqualTypeOf<{qux: number} | undefined>(); expectTypeOf>().toEqualTypeOf(); - // Test bracket notation expectTypeOf>().toBeNumber(); // NOTE: This would fail if `[0][0]` was converted into `00`: diff --git a/test-d/internal/is-number-like.ts b/test-d/internal/is-number-like.ts new file mode 100644 index 000000000..5ced14009 --- /dev/null +++ b/test-d/internal/is-number-like.ts @@ -0,0 +1,8 @@ +import {expectType} from 'tsd'; +import type {IsNumberLike} from '../../source/internal/numeric.d'; + +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); diff --git a/test-d/paths.ts b/test-d/paths.ts index 7e54ccff0..543265c5a 100644 --- a/test-d/paths.ts +++ b/test-d/paths.ts @@ -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}>; @@ -118,3 +118,31 @@ expectType<'foo'>(recursion0); declare const recursion1: Paths; expectType<'foo' | 'foo.foo'>(recursion1); + +// Test a[0].b style +type Object1 = { + arr: [{a: string}]; +}; +expectType>({} as 'arr' | 'arr[0]' | 'arr[0].a'); + +type Object2 = { + arr: Array<{a: string}>; + arr1: string[]; +}; +expectType>({} as 'arr' | 'arr1' | `arr[${number}]` | `arr[${number}].a` | `arr1[${number}]`); + +type Object3 = { + 1: 'foo'; + '2': 'bar'; +}; +expectType>({} as '[1]' | '[2]'); + +type deepArray = { + arr: Array>>; +}; +expectType>({} as 'arr' | `arr[${number}]` | `arr[${number}][${number}]` | `arr[${number}][${number}][${number}]` | `arr[${number}][${number}][${number}].a`); + +type RecursionArray = RecursionArray[]; +type RecursionArrayPaths = Paths; +expectAssignable({} as `[${number}][${number}][${number}][${number}]`); +expectNotAssignable({} as `[${number}][${number}][${number}][${number}][${number}]`);