From 5d8fb37405554d21f8d62636253de2197c3dcb15 Mon Sep 17 00:00:00 2001 From: Emiya Date: Thu, 1 Aug 2024 11:25:03 +0800 Subject: [PATCH 1/7] Path: add style option --- source/internal/numeric.d.ts | 19 +++++++++ source/paths.d.ts | 74 ++++++++++++++++++++++++++++-------- test-d/paths.ts | 30 ++++++++++++++- 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 089120fb5..5519dd402 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -19,6 +19,25 @@ NumberAbsolute */ export type NumberAbsolute = `${N}` extends `-${infer StringPositiveN}` ? StringToNumber : N; +/** +Return a type if it is a number or a number string + +@example +``` +type A = IsNumberLike<'1'>; +//=> true + +type B = IsNumberLike<1>; +//=> true + +type C = IsNumberLike<'a'>; +//=> false +*/ +export type IsNumberLike = + N extends number ? true + : N extends `${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..ca033ba9b 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -1,10 +1,10 @@ -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'; +import {type Sum} from './sum'; /** Paths options. @@ -18,6 +18,18 @@ export type PathsOptions = { @default 10 */ maxRecursionDepth?: number; + + /** + Style of the path. + + @default 'a.0.b' + */ + style?: 'a[0].b' | 'a.0.b'; +}; + +type DefaultPathsOptions = { + maxRecursionDepth: 10; + style: 'a.0.b'; }; /** @@ -61,7 +73,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 +95,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['style'] extends 'a[0].b' + ? IsNumberLike extends true + ? `[${Key}]` + : (Key | ToString) : never - ) + | + Options['style'] extends 'a.0.b' + // 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['style'] extends 'a[0].b' + ? SubPath extends `[${number}]` | `[${number}]${string}` + ? `${TranformedKey}${SubPath}` // If next node is number key like `[3]`, no need to add `.` before it. + : `${TranformedKey}.${SubPath}` + : never + ) | ( + Options['style'] extends 'a.0.b' + ? `${TranformedKey}.${SubPath}` + : never + ) + : never + : never + : never + ) + : never : never }[keyof T & (T extends UnknownArray ? number : unknown)] : never diff --git a/test-d/paths.ts b/test-d/paths.ts index 7e54ccff0..83924a88d 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}]`); From 07affd3aee98f080451c32dc8e96f0142f010ce9 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Thu, 1 Aug 2024 12:50:19 +0200 Subject: [PATCH 2/7] Update numeric.d.ts --- source/internal/numeric.d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 5519dd402..87aa363bf 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -20,7 +20,7 @@ NumberAbsolute export type NumberAbsolute = `${N}` extends `-${infer StringPositiveN}` ? StringToNumber : N; /** -Return a type if it is a number or a number string +Return a type if it is a number or a number string. @example ``` @@ -34,8 +34,10 @@ type C = IsNumberLike<'a'>; //=> false */ export type IsNumberLike = - N extends number ? true - : N extends `${number}` ? true + N extends number + ? true + : N extends `${number}` + ? true : false; /** From fa16ca30f17e453b4b42918839bb2f56d28147c3 Mon Sep 17 00:00:00 2001 From: Emiya Date: Thu, 8 Aug 2024 18:26:08 +0800 Subject: [PATCH 3/7] fix: cr --- source/internal/numeric.d.ts | 18 +++++++---- source/paths.d.ts | 50 +++++++++++++++++++++++-------- test-d/get.ts | 1 - test-d/internal/is-number-like.ts | 9 ++++++ test-d/paths.ts | 10 +++---- 5 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 test-d/internal/is-number-like.ts diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 87aa363bf..3b80885f8 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -22,23 +22,29 @@ export type NumberAbsolute = `${N}` extends `-${infer StringPo /** Return a type if it is a number or a number string. +supports floating-point as a string. + @example ``` type A = IsNumberLike<'1'>; //=> true -type B = IsNumberLike<1>; +type B = IsNumberLike<'-1.1'>; +//=> true + +type C = IsNumberLike<1>; //=> true -type C = IsNumberLike<'a'>; +type D = IsNumberLike<'a'>; //=> false */ export type IsNumberLike = - N extends number - ? true - : N extends `${number}` + N extends number ? true + : N extends `${number}` ? true - : false; + : 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 ca033ba9b..448b18672 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -20,16 +20,42 @@ export type PathsOptions = { maxRecursionDepth?: number; /** - Style of the path. + Use bracket notation for array indices or number key of object. - @default 'a.0.b' + @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]' + ``` */ - style?: 'a[0].b' | 'a.0.b'; + bracketNotation?: boolean; }; type DefaultPathsOptions = { maxRecursionDepth: 10; - style: 'a.0.b'; + bracketNotation: false; }; /** @@ -76,8 +102,8 @@ open('listB.1'); // TypeError. Because listB only has one element. export type Paths = _Paths; type _Paths> = @@ -104,13 +130,13 @@ type InternalPaths> = [Key in keyof T]: Key extends string | number // Limit `Key` to string or number. ? ( - Options['style'] extends 'a[0].b' + Options['bracketNotation'] extends true ? IsNumberLike extends true ? `[${Key}]` : (Key | ToString) : never | - Options['style'] extends 'a.0.b' + Options['bracketNotation'] extends false // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work. ? (Key | ToString) : never @@ -121,16 +147,16 @@ type InternalPaths> = | ( // Recursively generate paths for the current key GreaterThan extends true // Limit the depth to prevent infinite recursion - ? _Paths}> extends infer SubPath + ? _Paths}> extends infer SubPath ? SubPath extends string | number ? ( - Options['style'] extends 'a[0].b' - ? SubPath extends `[${number}]` | `[${number}]${string}` + 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['style'] extends 'a.0.b' + Options['bracketNotation'] extends false ? `${TranformedKey}.${SubPath}` : 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..4522195de --- /dev/null +++ b/test-d/internal/is-number-like.ts @@ -0,0 +1,9 @@ +import {expectType} from 'tsd'; +import type {IsNumberLike} from '../../source/internal/numeric.d'; + +type D = IsNumberLike<'a'>; +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); diff --git a/test-d/paths.ts b/test-d/paths.ts index 83924a88d..543265c5a 100644 --- a/test-d/paths.ts +++ b/test-d/paths.ts @@ -123,26 +123,26 @@ expectType<'foo' | 'foo.foo'>(recursion1); type Object1 = { arr: [{a: string}]; }; -expectType>({} as 'arr' | 'arr[0]' | 'arr[0].a'); +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}]`); +expectType>({} as 'arr' | 'arr1' | `arr[${number}]` | `arr[${number}].a` | `arr1[${number}]`); type Object3 = { 1: 'foo'; '2': 'bar'; }; -expectType>({} as '[1]' | '[2]'); +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`); +expectType>({} as 'arr' | `arr[${number}]` | `arr[${number}][${number}]` | `arr[${number}][${number}][${number}]` | `arr[${number}][${number}][${number}].a`); type RecursionArray = RecursionArray[]; -type RecursionArrayPaths = Paths; +type RecursionArrayPaths = Paths; expectAssignable({} as `[${number}][${number}][${number}][${number}]`); expectNotAssignable({} as `[${number}][${number}][${number}][${number}][${number}]`); From f923983700fc19fe8d30efbf769786dab5cd7b79 Mon Sep 17 00:00:00 2001 From: Emiya Date: Thu, 8 Aug 2024 18:27:38 +0800 Subject: [PATCH 4/7] typo --- source/internal/numeric.d.ts | 2 +- test-d/internal/is-number-like.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 3b80885f8..5e01a823d 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -22,7 +22,7 @@ export type NumberAbsolute = `${N}` extends `-${infer StringPo /** Return a type if it is a number or a number string. -supports floating-point as a string. +Supports floating-point as a string. @example ``` diff --git a/test-d/internal/is-number-like.ts b/test-d/internal/is-number-like.ts index 4522195de..5ced14009 100644 --- a/test-d/internal/is-number-like.ts +++ b/test-d/internal/is-number-like.ts @@ -1,7 +1,6 @@ import {expectType} from 'tsd'; import type {IsNumberLike} from '../../source/internal/numeric.d'; -type D = IsNumberLike<'a'>; expectType>(true); expectType>(true); expectType>(true); From 8c18c0c826a39d3f90d999847d9b8848fb4db1a5 Mon Sep 17 00:00:00 2001 From: Emiya Date: Thu, 8 Aug 2024 18:28:50 +0800 Subject: [PATCH 5/7] rm unused import --- source/paths.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/source/paths.d.ts b/source/paths.d.ts index 448b18672..7c907a2bd 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -4,7 +4,6 @@ import type {IsAny} from './is-any'; import type {UnknownArray} from './unknown-array'; import type {Subtract} from './subtract'; import type {GreaterThan} from './greater-than'; -import {type Sum} from './sum'; /** Paths options. From bc122449e9eb39a7eb1ba7a0cff0056981c68649 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Thu, 8 Aug 2024 13:46:52 +0200 Subject: [PATCH 6/7] Update numeric.d.ts --- source/internal/numeric.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 5e01a823d..b2e14882b 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -20,7 +20,7 @@ NumberAbsolute export type NumberAbsolute = `${N}` extends `-${infer StringPositiveN}` ? StringToNumber : N; /** -Return a type if it is a number or a number string. +Check whether the given type is a number or a number string. Supports floating-point as a string. From a315c2cf96dd4e216fc33cfe8d823260b0e91c43 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Thu, 8 Aug 2024 13:49:04 +0200 Subject: [PATCH 7/7] Update paths.d.ts --- source/paths.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/paths.d.ts b/source/paths.d.ts index 7c907a2bd..7e9f36599 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -19,7 +19,7 @@ export type PathsOptions = { maxRecursionDepth?: number; /** - Use bracket notation for array indices or number key of object. + Use bracket notation for array indices and numeric object keys. @default false