Skip to content

Commit 3b76ac9

Browse files
committed
fix: Paths with generic types
1 parent 77672ac commit 3b76ac9

File tree

4 files changed

+28
-46
lines changed

4 files changed

+28
-46
lines changed

source/internal/numeric.d.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {IsNever} from '../is-never.d.ts';
22
import type {Finite, NegativeInfinity, PositiveInfinity} from '../numeric.d.ts';
33
import type {UnknownArray} from '../unknown-array.d.ts';
44
import type {StringToNumber} from './string.d.ts';
5-
import type {IsAnyOrNever} from './type.d.ts';
5+
import type {IfNotAnyOrNever, IsAnyOrNever} from './type.d.ts';
66

77
/**
88
Returns the absolute value of a given value.
@@ -44,10 +44,11 @@ type E = IsNumberLike<'a'>;
4444
//=> false
4545
*/
4646
export type IsNumberLike<N> =
47-
IsAnyOrNever<N> extends true ? N
48-
: N extends number | `${number}`
47+
IfNotAnyOrNever<N,
48+
N extends number | `${number}`
4949
? true
50-
: false;
50+
: false,
51+
boolean, false>;
5152

5253
/**
5354
Returns the minimum number in the given union of numbers.

source/paths.d.ts

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts';
22
import type {IsAny} from './is-any.d.ts';
33
import type {UnknownArray} from './unknown-array.d.ts';
4-
import type {Subtract} from './subtract.d.ts';
54
import type {GreaterThan} from './greater-than.d.ts';
65
import type {IsNever} from './is-never.d.ts';
6+
import type {Sum} from './sum.d.ts';
7+
import type {And} from './and.d.ts';
78

89
/**
910
Paths options.
@@ -190,21 +191,23 @@ open('listB.1'); // TypeError. Because listB only has one element.
190191
*/
191192
export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, ApplyDefaultOptions<PathsOptions, DefaultPathsOptions, Options>>;
192193

193-
type _Paths<T, Options extends Required<PathsOptions>> =
194+
type _Paths<T, Options extends Required<PathsOptions>, CurrentDepth extends number = 0> =
194195
T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
195196
? never
196197
: IsAny<T> extends true
197198
? never
198199
: T extends UnknownArray
199200
? number extends T['length']
200201
// We need to handle the fixed and non-fixed index part of the array separately.
201-
? InternalPaths<StaticPartOfArray<T>, Options> | InternalPaths<Array<VariablePartOfArray<T>[number]>, Options>
202-
: InternalPaths<T, Options>
202+
?
203+
| InternalPaths<StaticPartOfArray<T>, Options, CurrentDepth>
204+
| InternalPaths<Array<VariablePartOfArray<T>[number]>, Options, CurrentDepth>
205+
: InternalPaths<T, Options, CurrentDepth>
203206
: T extends object
204-
? InternalPaths<T, Options>
207+
? InternalPaths<T, Options, CurrentDepth>
205208
: never;
206209

207-
type InternalPaths<T, Options extends Required<PathsOptions>> =
210+
type InternalPaths<T, Options extends Required<PathsOptions>, CurrentDepth extends number> =
208211
Options['maxRecursionDepth'] extends infer MaxDepth extends number
209212
? Required<T> extends infer T
210213
? T extends readonly []
@@ -215,19 +218,17 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
215218
[Key in keyof T]:
216219
Key extends string | number // Limit `Key` to string or number.
217220
? (
218-
Options['bracketNotation'] extends true
219-
? IsNumberLike<Key> extends true
220-
? `[${Key}]`
221-
: (Key | ToString<Key>)
222-
: Options['bracketNotation'] extends false
221+
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
222+
? `[${Key}]`
223223
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
224-
? (Key | ToString<Key>)
225-
: never
224+
: CurrentDepth extends 0
225+
? Key | ToString<Key>
226+
: `.${(Key | ToString<Key>)}`
226227
) extends infer TranformedKey extends string | number ?
227228
// 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
228229
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
229230
| ((Options['leavesOnly'] extends true
230-
? MaxDepth extends 0
231+
? MaxDepth extends CurrentDepth
231232
? TranformedKey
232233
: T[Key] extends infer Value
233234
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
@@ -238,36 +239,16 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
238239
: never
239240
: TranformedKey
240241
) extends infer _TransformedKey
241-
// If `depth` is provided, the condition becomes truthy only when it reaches `0`.
242+
// If `depth` is provided, the condition becomes truthy only when it reaches `CurrentDepth`.
242243
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
243-
? 0 extends Options['depth']
244+
? CurrentDepth extends Options['depth']
244245
? _TransformedKey
245246
: never
246247
: never)
247248
| (
248249
// Recursively generate paths for the current key
249-
GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
250-
? _Paths<T[Key],
251-
{
252-
bracketNotation: Options['bracketNotation'];
253-
maxRecursionDepth: Subtract<MaxDepth, 1>;
254-
leavesOnly: Options['leavesOnly'];
255-
depth: Subtract<Options['depth'], 1>;
256-
}> extends infer SubPath
257-
? SubPath extends string | number
258-
? (
259-
Options['bracketNotation'] extends true
260-
? SubPath extends `[${any}]` | `[${any}]${string}`
261-
? `${TranformedKey}${SubPath}` // If next node is number key like `[3]`, no need to add `.` before it.
262-
: `${TranformedKey}.${SubPath}`
263-
: never
264-
) | (
265-
Options['bracketNotation'] extends false
266-
? `${TranformedKey}.${SubPath}`
267-
: never
268-
)
269-
: never
270-
: never
250+
GreaterThan<MaxDepth, CurrentDepth> extends true // Limit the depth to prevent infinite recursion
251+
? `${TranformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
271252
: never
272253
)
273254
: never

test-d/internal/is-number-like.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ expectType<IsNumberLike<'5+1.2'>>(false);
3636
expectType<IsNumberLike<'5e-3.1'>>(false);
3737

3838
// Edge cases
39-
expectType<IsNumberLike<never>>({} as never);
40-
expectType<IsNumberLike<any>>({} as any);
39+
expectType<IsNumberLike<never>>(false);
40+
expectType<IsNumberLike<any>>({} as boolean);
4141
expectType<IsNumberLike<number>>(true);
4242
expectType<IsNumberLike<PositiveInfinity>>(true);
4343
expectType<IsNumberLike<NegativeInfinity>>(true);

test-d/paths.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase<string>]: {a:
417417
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys1); // Collapsed union
418418

419419
declare const nonRootIndexSignature: Paths<{a: {[x: string]: {b: string; c: number}}}>;
420-
expectType<'a' | `a.${string}`>(nonRootIndexSignature); // Collapsed union
420+
expectType<'a' | `a.${string}` | `a.${string}.b` | `a.${string}.c`>(nonRootIndexSignature);
421421

422422
declare const nonRootIndexSignature1: Paths<{a: {[x: Lowercase<string>]: {b: string; c: number}}}>;
423423
expectType<'a' | `a.${Lowercase<string>}` | `a.${Lowercase<string>}.b` | `a.${Lowercase<string>}.c`>(nonRootIndexSignature1);
@@ -443,7 +443,7 @@ declare const indexSignatureLeaves1: Paths<{a: {[x: string]: {b: string; c: numb
443443
expectType<`a.${string}.b` | `a.${string}.c` | 'd' | 'e.f'>(indexSignatureLeaves1);
444444

445445
declare const indexSignatureLeaves2: Paths<{a: {[x: string]: [] | {b: number}}}, {leavesOnly: true}>;
446-
expectType<`a.${string}`>(indexSignatureLeaves2); // Collapsed union
446+
expectType<`a.${string}` | `a.${string}.b`>(indexSignatureLeaves2);
447447

448448
declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>;
449449
expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth);

0 commit comments

Comments
 (0)