From c4685a7d0dab3301e36baf439f298c3cb9ed0862 Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Thu, 29 Jan 2026 23:48:19 +0530 Subject: [PATCH 1/3] fix: `Paths` with generic types --- source/internal/numeric.d.ts | 9 ++--- source/paths.d.ts | 55 ++++++++++--------------------- test-d/internal/is-number-like.ts | 4 +-- test-d/paths.ts | 16 +++++++-- 4 files changed, 38 insertions(+), 46 deletions(-) diff --git a/source/internal/numeric.d.ts b/source/internal/numeric.d.ts index 0c6cdfcf8..cb06e1128 100644 --- a/source/internal/numeric.d.ts +++ b/source/internal/numeric.d.ts @@ -2,7 +2,7 @@ import type {IsNever} from '../is-never.d.ts'; import type {Finite, NegativeInfinity, PositiveInfinity} from '../numeric.d.ts'; import type {UnknownArray} from '../unknown-array.d.ts'; import type {StringToNumber} from './string.d.ts'; -import type {IsAnyOrNever} from './type.d.ts'; +import type {IfNotAnyOrNever, IsAnyOrNever} from './type.d.ts'; /** Returns the absolute value of a given value. @@ -44,10 +44,11 @@ type E = IsNumberLike<'a'>; //=> false */ export type IsNumberLike = - IsAnyOrNever extends true ? N - : N extends number | `${number}` + IfNotAnyOrNever; /** Returns the minimum number in the given union of numbers. diff --git a/source/paths.d.ts b/source/paths.d.ts index a624777f5..07f5eab9b 100644 --- a/source/paths.d.ts +++ b/source/paths.d.ts @@ -1,9 +1,10 @@ import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts'; import type {IsAny} from './is-any.d.ts'; import type {UnknownArray} from './unknown-array.d.ts'; -import type {Subtract} from './subtract.d.ts'; import type {GreaterThan} from './greater-than.d.ts'; import type {IsNever} from './is-never.d.ts'; +import type {Sum} from './sum.d.ts'; +import type {And} from './and.d.ts'; /** Paths options. @@ -190,7 +191,7 @@ open('listB.1'); // TypeError. Because listB only has one element. */ export type Paths = _Paths>; -type _Paths> = +type _Paths, CurrentDepth extends number = 0> = T extends NonRecursiveType | ReadonlyMap | ReadonlySet ? never : IsAny extends true @@ -198,13 +199,13 @@ type _Paths> = : T extends UnknownArray ? number extends T['length'] // We need to handle the fixed and non-fixed index part of the array separately. - ? InternalPaths, Options> | InternalPaths[number]>, Options> - : InternalPaths + ? InternalPaths, Options, CurrentDepth> | InternalPaths[number]>, Options, CurrentDepth> + : InternalPaths : T extends object - ? InternalPaths + ? InternalPaths : never; -type InternalPaths> = +type InternalPaths, CurrentDepth extends number> = Options['maxRecursionDepth'] extends infer MaxDepth extends number ? Required extends infer T ? T extends readonly [] @@ -215,19 +216,17 @@ type InternalPaths> = [Key in keyof T]: Key extends string | number // Limit `Key` to string or number. ? ( - Options['bracketNotation'] extends true - ? IsNumberLike extends true - ? `[${Key}]` - : (Key | ToString) - : Options['bracketNotation'] extends false + And> extends true + ? `[${Key}]` // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work. - ? (Key | ToString) - : never + : CurrentDepth extends 0 + ? Key | ToString + : `.${(Key | ToString)}` ) 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 - ? MaxDepth extends 0 + ? MaxDepth extends CurrentDepth ? TranformedKey : T[Key] extends infer Value ? (Value extends readonly [] | NonRecursiveType | ReadonlyMap | ReadonlySet @@ -238,36 +237,16 @@ type InternalPaths> = : never : TranformedKey ) extends infer _TransformedKey - // If `depth` is provided, the condition becomes truthy only when it reaches `0`. + // If `depth` is provided, the condition becomes truthy only when it reaches `CurrentDepth`. // Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths. - ? 0 extends Options['depth'] + ? CurrentDepth extends Options['depth'] ? _TransformedKey : never : never) | ( // Recursively generate paths for the current key - GreaterThan extends true // Limit the depth to prevent infinite recursion - ? _Paths; - leavesOnly: Options['leavesOnly']; - depth: Subtract; - }> 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 + GreaterThan extends true // Limit the depth to prevent infinite recursion + ? `${TranformedKey}${_Paths> & (string | number)}` : never ) : never diff --git a/test-d/internal/is-number-like.ts b/test-d/internal/is-number-like.ts index 88bb1b96c..2919d7bd3 100644 --- a/test-d/internal/is-number-like.ts +++ b/test-d/internal/is-number-like.ts @@ -36,8 +36,8 @@ expectType>(false); expectType>(false); // Edge cases -expectType>({} as never); -expectType>({} as any); +expectType>(false); +expectType>({} as boolean); expectType>(true); expectType>(true); expectType>(true); diff --git a/test-d/paths.ts b/test-d/paths.ts index 3dc450e9b..c0a379f74 100644 --- a/test-d/paths.ts +++ b/test-d/paths.ts @@ -417,7 +417,7 @@ declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase]: {a: expectType | `${Uppercase}.a` | `${Uppercase}.b`>(indexSignatureWithStaticKeys1); // Collapsed union declare const nonRootIndexSignature: Paths<{a: {[x: string]: {b: string; c: number}}}>; -expectType<'a' | `a.${string}`>(nonRootIndexSignature); // Collapsed union +expectType<'a' | `a.${string}` | `a.${string}.b` | `a.${string}.c`>(nonRootIndexSignature); declare const nonRootIndexSignature1: Paths<{a: {[x: Lowercase]: {b: string; c: number}}}>; expectType<'a' | `a.${Lowercase}` | `a.${Lowercase}.b` | `a.${Lowercase}.c`>(nonRootIndexSignature1); @@ -443,7 +443,7 @@ declare const indexSignatureLeaves1: Paths<{a: {[x: string]: {b: string; c: numb expectType<`a.${string}.b` | `a.${string}.c` | 'd' | 'e.f'>(indexSignatureLeaves1); declare const indexSignatureLeaves2: Paths<{a: {[x: string]: [] | {b: number}}}, {leavesOnly: true}>; -expectType<`a.${string}`>(indexSignatureLeaves2); // Collapsed union +expectType<`a.${string}` | `a.${string}.b`>(indexSignatureLeaves2); declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>; expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth); @@ -462,3 +462,15 @@ expectType<`a.${string}.b`>(indexSignatureDepth4); declare const indexSignatureDepthLeaves: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2; leavesOnly: true}>; expectType<`a.${string}.b` | `a.${string}.c` | 'd'>(indexSignatureDepthLeaves); + +// Generic types +type SomeTypeWithConstraint> = never; + +type Foo = {bar: {baz: T}}; +type Test1 = SomeTypeWithConstraint, 'bar.baz'>; + +type Bar = {bar: {baz: {qux: T}; fizz: {buzz: T} | T}}; +type Test2 = SomeTypeWithConstraint< + Bar, + 'bar' | 'bar.baz' | 'bar.baz.qux' | 'bar.fizz' | 'bar.fizz.buzz' +>; From e361150be69e9fc0020d5512c14879c3058d48db Mon Sep 17 00:00:00 2001 From: Som Shekhar Mukherjee Date: Fri, 30 Jan 2026 21:21:15 +0530 Subject: [PATCH 2/3] test: improve generic case --- test-d/paths.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-d/paths.ts b/test-d/paths.ts index c0a379f74..21bd5ce76 100644 --- a/test-d/paths.ts +++ b/test-d/paths.ts @@ -469,8 +469,8 @@ type SomeTypeWithConstraint> = never; type Foo = {bar: {baz: T}}; type Test1 = SomeTypeWithConstraint, 'bar.baz'>; -type Bar = {bar: {baz: {qux: T}; fizz: {buzz: T} | T}}; -type Test2 = SomeTypeWithConstraint< - Bar, +type Bar = {bar: {baz: {qux: T}; fizz: {buzz: U} | U | T}}; +type Test2 = SomeTypeWithConstraint< + Bar, 'bar' | 'bar.baz' | 'bar.baz.qux' | 'bar.fizz' | 'bar.fizz.buzz' >; From 2f93f4b2f3c2d424acbd0efe3e01c451c0d47240 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sat, 31 Jan 2026 11:47:27 +0700 Subject: [PATCH 3/3] Update paths.ts --- test-d/paths.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test-d/paths.ts b/test-d/paths.ts index 21bd5ce76..11a300258 100644 --- a/test-d/paths.ts +++ b/test-d/paths.ts @@ -137,6 +137,20 @@ type Object3 = { }; expectType>({} as '[1]' | '[2]'); +type Object4 = { + 1: { + a: string; + }; +}; +expectType>({} as '[1]' | '[1].a'); + +type Object5 = { + 1: { + 2: string; + }; +}; +expectType>({} as '[1]' | '[1][2]'); + type deepArray = { arr: Array>>; };