Skip to content

Commit 9926e5d

Browse files
authored
Paths: Fix behavior with index signatures (#1193)
1 parent 5067e25 commit 9926e5d

File tree

2 files changed

+102
-21
lines changed

2 files changed

+102
-21
lines changed

source/paths.d.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike, ApplyDefaultOptions} from './internal/index.d.ts';
2-
import type {EmptyObject} from './empty-object.d.ts';
32
import type {IsAny} from './is-any.d.ts';
43
import type {UnknownArray} from './unknown-array.d.ts';
54
import type {Subtract} from './subtract.d.ts';
65
import type {GreaterThan} from './greater-than.d.ts';
6+
import type {IsNever} from './is-never.d.ts';
77

88
/**
99
Paths options.
@@ -195,28 +195,34 @@ type _Paths<T, Options extends Required<PathsOptions>> =
195195
type InternalPaths<T, Options extends Required<PathsOptions>> =
196196
Options['maxRecursionDepth'] extends infer MaxDepth extends number
197197
? Required<T> extends infer T
198-
? T extends EmptyObject | readonly []
198+
? T extends readonly []
199199
? never
200-
: {
201-
[Key in keyof T]:
202-
Key extends string | number // Limit `Key` to string or number.
203-
? (
204-
Options['bracketNotation'] extends true
205-
? IsNumberLike<Key> extends true
206-
? `[${Key}]`
207-
: (Key | ToString<Key>)
208-
: Options['bracketNotation'] extends false
209-
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
210-
? (Key | ToString<Key>)
211-
: never
212-
) extends infer TranformedKey extends string | number ?
213-
// 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
214-
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
200+
: IsNever<keyof T> extends true // Check for empty object
201+
? never
202+
: {
203+
[Key in keyof T]:
204+
Key extends string | number // Limit `Key` to string or number.
205+
? (
206+
Options['bracketNotation'] extends true
207+
? IsNumberLike<Key> extends true
208+
? `[${Key}]`
209+
: (Key | ToString<Key>)
210+
: Options['bracketNotation'] extends false
211+
// If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
212+
? (Key | ToString<Key>)
213+
: never
214+
) extends infer TranformedKey extends string | number ?
215+
// 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
216+
// 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
215217
| ((Options['leavesOnly'] extends true
216218
? MaxDepth extends 0
217219
? TranformedKey
218-
: T[Key] extends EmptyObject | readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
219-
? TranformedKey
220+
: T[Key] extends infer Value
221+
? (Value extends readonly [] | NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
222+
? TranformedKey
223+
: IsNever<keyof Value> extends true // Check for empty object
224+
? TranformedKey
225+
: never)
220226
: never
221227
: TranformedKey
222228
) extends infer _TransformedKey
@@ -252,8 +258,8 @@ type InternalPaths<T, Options extends Required<PathsOptions>> =
252258
: never
253259
: never
254260
)
261+
: never
255262
: never
256-
: never
257-
}[keyof T & (T extends UnknownArray ? number : unknown)]
263+
}[keyof T & (T extends UnknownArray ? number : unknown)]
258264
: never
259265
: never;

test-d/paths.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ expectType<'a.b' | 'a.c' | 'a.d' | 'a.e' | 'a.f.g' | 'h'>(leaves);
155155
declare const unionLeaves: Paths<{a: {b?: number}} | {a: string; b?: {c: string}}, {leavesOnly: true}>;
156156
expectType<'a.b' | 'a' | 'b.c'>(unionLeaves);
157157

158+
declare const unionLeaves1: Paths<{a: {} | {c: number}}, {leavesOnly: true}>;
159+
expectType<'a' | 'a.c'>(unionLeaves1);
160+
161+
declare const unionLeaves2: Paths<{a: {[x: string]: number} | {c: number}}, {leavesOnly: true}>;
162+
expectType<`a.${string}`>(unionLeaves2); // Collapsed union
163+
158164
declare const emptyObjectLeaves: Paths<{a: {}}, {leavesOnly: true}>;
159165
expectType<'a'>(emptyObjectLeaves);
160166

@@ -387,3 +393,72 @@ expectType<never>(neverDepth);
387393

388394
declare const anyDepth: Paths<DeepObject, {depth: any}>;
389395
expectType<'a' | 'a.b.c' | `a.b2.${number}` | 'a.b3' | 'a.b' | 'a.b2' | 'a.b.c.d'>(anyDepth);
396+
397+
// Index signatures
398+
declare const indexSignature: Paths<{[x: string]: {a: string; b: number}}>;
399+
expectType<string>(indexSignature); // Collapsed union
400+
401+
declare const indexSignature1: Paths<{[x: Lowercase<string>]: {a: string; b: number}}>;
402+
expectType<Lowercase<string> | `${Lowercase<string>}.a` | `${Lowercase<string>}.b`>(indexSignature1);
403+
404+
declare const indexSignature2: Paths<{[x: number]: {0: string; 1: number}}>;
405+
expectType<number | `${number}` | `${number}.0` | `${number}.1`>(indexSignature2);
406+
407+
declare const indexSignature3: Paths<{[x: Uppercase<string>]: {a: string; b: number}}>;
408+
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignature3);
409+
410+
declare const indexSignature4: Paths<{a: {[x: symbol]: {b: number; c: number}}}>;
411+
expectType<'a'>(indexSignature4);
412+
413+
declare const indexSignatureWithStaticKeys: Paths<{[x: Uppercase<string>]: {a: string; b: number}; c: number}>;
414+
expectType<'c' | Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys);
415+
416+
declare const indexSignatureWithStaticKeys1: Paths<{[x: Uppercase<string>]: {a: string; b?: number}; C: {a: 'a'}}>;
417+
expectType<Uppercase<string> | `${Uppercase<string>}.a` | `${Uppercase<string>}.b`>(indexSignatureWithStaticKeys1); // Collapsed union
418+
419+
declare const nonRootIndexSignature: Paths<{a: {[x: string]: {b: string; c: number}}}>;
420+
expectType<'a' | `a.${string}`>(nonRootIndexSignature); // Collapsed union
421+
422+
declare const nonRootIndexSignature1: Paths<{a: {[x: Lowercase<string>]: {b: string; c: number}}}>;
423+
expectType<'a' | `a.${Lowercase<string>}` | `a.${Lowercase<string>}.b` | `a.${Lowercase<string>}.c`>(nonRootIndexSignature1);
424+
425+
declare const nestedIndexSignature: Paths<{[x: string]: {[x: Lowercase<string>]: {a: string; b: number}}}>;
426+
expectType<string>(nestedIndexSignature);
427+
428+
declare const nestedIndexSignature1: Paths<{[x: Uppercase<string>]: {[x: Lowercase<string>]: {a: string; b: number}}}>;
429+
expectType<Uppercase<string> | `${Uppercase<string>}.${Lowercase<string>}` | `${Uppercase<string>}.${Lowercase<string>}.a` | `${Uppercase<string>}.${Lowercase<string>}.b`>(
430+
nestedIndexSignature1,
431+
);
432+
433+
declare const indexSignatureUnion: Paths<{a: {[x: string]: number} | {b: number}}>;
434+
expectType<'a' | `a.${string}`>(indexSignatureUnion); // Collapsed union
435+
436+
declare const indexSignatureUnion1: Paths<{a: {[x: Uppercase<string>]: number} | {b: number}}>;
437+
expectType<'a' | 'a.b' | `a.${Uppercase<string>}`>(indexSignatureUnion1);
438+
439+
declare const indexSignatureLeaves: Paths<{[x: string]: {a: string; b: number}}, {leavesOnly: true}>;
440+
expectType<`${string}.a` | `${string}.b`>(indexSignatureLeaves);
441+
442+
declare const indexSignatureLeaves1: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {leavesOnly: true}>;
443+
expectType<`a.${string}.b` | `a.${string}.c` | 'd' | 'e.f'>(indexSignatureLeaves1);
444+
445+
declare const indexSignatureLeaves2: Paths<{a: {[x: string]: [] | {b: number}}}, {leavesOnly: true}>;
446+
expectType<`a.${string}`>(indexSignatureLeaves2); // Collapsed union
447+
448+
declare const indexSignatureDepth: Paths<{[x: string]: {a: string; b: number}}, {depth: 1}>;
449+
expectType<`${string}.b` | `${string}.a`>(indexSignatureDepth);
450+
451+
declare const indexSignatureDepth1: Paths<{[x: string]: {a: string; b: number}}, {depth: 0}>;
452+
expectType<string>(indexSignatureDepth1);
453+
454+
declare const indexSignatureDepth2: Paths<{[x: string]: {a: string; b: number}}, {depth: 0 | 1}>;
455+
expectType<string>(indexSignatureDepth2); // Collapsed union
456+
457+
declare const indexSignatureDepth3: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2}>;
458+
expectType<'a' | `a.${string}.b` | `a.${string}.c` | 'd' | 'e'>(indexSignatureDepth3);
459+
460+
declare const indexSignatureDepth4: Paths<{a: {[x: string]: [] | {b: number}}}, {depth: 2}>;
461+
expectType<`a.${string}.b`>(indexSignatureDepth4);
462+
463+
declare const indexSignatureDepthLeaves: Paths<{a: {[x: string]: {b: string; c: number}}; d: string; e: {f: number}}, {depth: 0 | 2; leavesOnly: true}>;
464+
expectType<`a.${string}.b` | `a.${string}.c` | 'd'>(indexSignatureDepthLeaves);

0 commit comments

Comments
 (0)