Skip to content

Commit 8f0419c

Browse files
authored
Paths: Fix behavior with generic types (#1343)
1 parent 77672ac commit 8f0419c

File tree

4 files changed

+52
-46
lines changed

4 files changed

+52
-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: 17 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,21 @@ 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+
? InternalPaths<StaticPartOfArray<T>, Options, CurrentDepth> | InternalPaths<Array<VariablePartOfArray<T>[number]>, Options, CurrentDepth>
203+
: InternalPaths<T, Options, CurrentDepth>
203204
: T extends object
204-
? InternalPaths<T, Options>
205+
? InternalPaths<T, Options, CurrentDepth>
205206
: never;
206207

207-
type InternalPaths<T, Options extends Required<PathsOptions>> =
208+
type InternalPaths<T, Options extends Required<PathsOptions>, CurrentDepth extends number> =
208209
Options['maxRecursionDepth'] extends infer MaxDepth extends number
209210
? Required<T> extends infer T
210211
? T extends readonly []
@@ -215,19 +216,17 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
215216
[Key in keyof T]:
216217
Key extends string | number // Limit `Key` to string or number.
217218
? (
218-
Options['bracketNotation'] extends true
219-
? IsNumberLike<Key> extends true
220-
? `[${Key}]`
221-
: (Key | ToString<Key>)
222-
: Options['bracketNotation'] extends false
219+
And<Options['bracketNotation'], IsNumberLike<Key>> extends true
220+
? `[${Key}]`
223221
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
224-
? (Key | ToString<Key>)
225-
: never
222+
: CurrentDepth extends 0
223+
? Key | ToString<Key>
224+
: `.${(Key | ToString<Key>)}`
226225
) extends infer TranformedKey extends string | number ?
227226
// 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
228227
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
229228
| ((Options['leavesOnly'] extends true
230-
? MaxDepth extends 0
229+
? MaxDepth extends CurrentDepth
231230
? TranformedKey
232231
: T[Key] extends infer Value
233232
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
@@ -238,36 +237,16 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
238237
: never
239238
: TranformedKey
240239
) extends infer _TransformedKey
241-
// If `depth` is provided, the condition becomes truthy only when it reaches `0`.
240+
// If `depth` is provided, the condition becomes truthy only when it reaches `CurrentDepth`.
242241
// Otherwise, since `depth` defaults to `number`, the condition is always truthy, returning paths at all depths.
243-
? 0 extends Options['depth']
242+
? CurrentDepth extends Options['depth']
244243
? _TransformedKey
245244
: never
246245
: never)
247246
| (
248247
// 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
248+
GreaterThan<MaxDepth, CurrentDepth> extends true // Limit the depth to prevent infinite recursion
249+
? `${TranformedKey}${_Paths<T[Key], Options, Sum<CurrentDepth, 1>> & (string | number)}`
271250
: never
272251
)
273252
: 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: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,20 @@ type Object3 = {
137137
};
138138
expectType<Paths<Object3, {bracketNotation: true}>>({} as '[1]' | '[2]');
139139

140+
type Object4 = {
141+
1: {
142+
a: string;
143+
};
144+
};
145+
expectType<Paths<Object4, {bracketNotation: true}>>({} as '[1]' | '[1].a');
146+
147+
type Object5 = {
148+
1: {
149+
2: string;
150+
};
151+
};
152+
expectType<Paths<Object5, {bracketNotation: true}>>({} as '[1]' | '[1][2]');
153+
140154
type deepArray = {
141155
arr: Array<Array<Array<{a: string}>>>;
142156
};
@@ -417,7 +431,7 @@ declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase<string>]: {a:
417431
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys1); // Collapsed union
418432

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

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

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

448462
declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>;
449463
expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth);
@@ -462,3 +476,15 @@ expectType<`a.${string}.b`>(indexSignatureDepth4);
462476

463477
declare const indexSignatureDepthLeaves: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2; leavesOnly: true}>;
464478
expectType<`a.${string}.b` | `a.${string}.c` | 'd'>(indexSignatureDepthLeaves);
479+
480+
// Generic types
481+
type SomeTypeWithConstraint<T, _U extends Paths<T>> = never;
482+
483+
type Foo<T> = {bar: {baz: T}};
484+
type Test1<T> = SomeTypeWithConstraint<Foo<T>, 'bar.baz'>;
485+
486+
type Bar<T, U> = {bar: {baz: {qux: T}; fizz: {buzz: U} | U | T}};
487+
type Test2<T, U> = SomeTypeWithConstraint<
488+
Bar<T, U>,
489+
'bar' | 'bar.baz' | 'bar.baz.qux' | 'bar.fizz' | 'bar.fizz.buzz'
490+
>;

0 commit comments

Comments
 (0)