UNPKG

5.11 kBTypeScriptView Raw
1import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike} from './internal';
2import type {EmptyObject} from './empty-object';
3import type {IsAny} from './is-any';
4import type {UnknownArray} from './unknown-array';
5import type {Subtract} from './subtract';
6import type {GreaterThan} from './greater-than';
7
8/**
9Paths options.
10
11@see {@link Paths}
12*/
13export type PathsOptions = {
14 /**
15 The maximum depth to recurse when searching for paths.
16
17 @default 10
18 */
19 maxRecursionDepth?: number;
20
21 /**
22 Use bracket notation for array indices and numeric object keys.
23
24 @default false
25
26 @example
27 ```
28 type ArrayExample = {
29 array: ['foo'];
30 };
31
32 type A = Paths<ArrayExample, {bracketNotation: false}>;
33 //=> 'array' | 'array.0'
34
35 type B = Paths<ArrayExample, {bracketNotation: true}>;
36 //=> 'array' | 'array[0]'
37 ```
38
39 @example
40 ```
41 type NumberKeyExample = {
42 1: ['foo'];
43 };
44
45 type A = Paths<NumberKeyExample, {bracketNotation: false}>;
46 //=> 1 | '1' | '1.0'
47
48 type B = Paths<NumberKeyExample, {bracketNotation: true}>;
49 //=> '[1]' | '[1][0]'
50 ```
51 */
52 bracketNotation?: boolean;
53};
54
55type DefaultPathsOptions = {
56 maxRecursionDepth: 10;
57 bracketNotation: false;
58};
59
60/**
61Generate a union of all possible paths to properties in the given object.
62
63It also works with arrays.
64
65Use-case: You want a type-safe way to access deeply nested properties in an object.
66
67@example
68```
69import type {Paths} from 'type-fest';
70
71type Project = {
72 filename: string;
73 listA: string[];
74 listB: [{filename: string}];
75 folder: {
76 subfolder: {
77 filename: string;
78 };
79 };
80};
81
82type ProjectPaths = Paths<Project>;
83//=> 'filename' | 'listA' | 'listB' | 'folder' | `listA.${number}` | 'listB.0' | 'listB.0.filename' | 'folder.subfolder' | 'folder.subfolder.filename'
84
85declare function open<Path extends ProjectPaths>(path: Path): void;
86
87open('filename'); // Pass
88open('folder.subfolder'); // Pass
89open('folder.subfolder.filename'); // Pass
90open('foo'); // TypeError
91
92// Also works with arrays
93open('listA.1'); // Pass
94open('listB.0'); // Pass
95open('listB.1'); // TypeError. Because listB only has one element.
96```
97
98@category Object
99@category Array
100*/
101export type Paths<T, Options extends PathsOptions = {}> = _Paths<T, {
102 // Set default maxRecursionDepth to 10
103 maxRecursionDepth: Options['maxRecursionDepth'] extends number ? Options['maxRecursionDepth'] : DefaultPathsOptions['maxRecursionDepth'];
104 // Set default bracketNotation to false
105 bracketNotation: Options['bracketNotation'] extends boolean ? Options['bracketNotation'] : DefaultPathsOptions['bracketNotation'];
106}>;
107
108type _Paths<T, Options extends Required<PathsOptions>> =
109 T extends NonRecursiveType | ReadonlyMap<unknown, unknown> | ReadonlySet<unknown>
110 ? never
111 : IsAny<T> extends true
112 ? never
113 : T extends UnknownArray
114 ? number extends T['length']
115 // We need to handle the fixed and non-fixed index part of the array separately.
116 ? InternalPaths<StaticPartOfArray<T>, Options>
117 | InternalPaths<Array<VariablePartOfArray<T>[number]>, Options>
118 : InternalPaths<T, Options>
119 : T extends object
120 ? InternalPaths<T, Options>
121 : never;
122
123type InternalPaths<T, Options extends Required<PathsOptions>> =
124 Options['maxRecursionDepth'] extends infer MaxDepth extends number
125 ? Required<T> extends infer T
126 ? T extends EmptyObject | readonly []
127 ? never
128 : {
129 [Key in keyof T]:
130 Key extends string | number // Limit `Key` to string or number.
131 ? (
132 Options['bracketNotation'] extends true
133 ? IsNumberLike<Key> extends true
134 ? `[${Key}]`
135 : (Key | ToString<Key>)
136 : never
137 |
138 Options['bracketNotation'] extends false
139 // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` work.
140 ? (Key | ToString<Key>)
141 : never
142 ) extends infer TranformedKey extends string | number ?
143 // 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
144 // 2. If style is 'a.0.b', transform 'Key' to `${Key}` | Key
145 | TranformedKey
146 | (
147 // Recursively generate paths for the current key
148 GreaterThan<MaxDepth, 0> extends true // Limit the depth to prevent infinite recursion
149 ? _Paths<T[Key], {bracketNotation: Options['bracketNotation']; maxRecursionDepth: Subtract<MaxDepth, 1>}> extends infer SubPath
150 ? SubPath extends string | number
151 ? (
152 Options['bracketNotation'] extends true
153 ? SubPath extends `[${any}]` | `[${any}]${string}`
154 ? `${TranformedKey}${SubPath}` // If next node is number key like `[3]`, no need to add `.` before it.
155 : `${TranformedKey}.${SubPath}`
156 : never
157 ) | (
158 Options['bracketNotation'] extends false
159 ? `${TranformedKey}.${SubPath}`
160 : never
161 )
162 : never
163 : never
164 : never
165 )
166 : never
167 : never
168 }[keyof T & (T extends UnknownArray ? number : unknown)]
169 : never
170 : never;