1 | import type {StaticPartOfArray, VariablePartOfArray, NonRecursiveType, ToString, IsNumberLike} from './internal';
|
2 | import type {EmptyObject} from './empty-object';
|
3 | import type {IsAny} from './is-any';
|
4 | import type {UnknownArray} from './unknown-array';
|
5 | import type {Subtract} from './subtract';
|
6 | import type {GreaterThan} from './greater-than';
|
7 |
|
8 | /**
|
9 | Paths options.
|
10 |
|
11 | @see {@link Paths}
|
12 | */
|
13 | export 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 |
|
55 | type DefaultPathsOptions = {
|
56 | maxRecursionDepth: 10;
|
57 | bracketNotation: false;
|
58 | };
|
59 |
|
60 | /**
|
61 | Generate a union of all possible paths to properties in the given object.
|
62 |
|
63 | It also works with arrays.
|
64 |
|
65 | Use-case: You want a type-safe way to access deeply nested properties in an object.
|
66 |
|
67 | @example
|
68 | ```
|
69 | import type {Paths} from 'type-fest';
|
70 |
|
71 | type Project = {
|
72 | filename: string;
|
73 | listA: string[];
|
74 | listB: [{filename: string}];
|
75 | folder: {
|
76 | subfolder: {
|
77 | filename: string;
|
78 | };
|
79 | };
|
80 | };
|
81 |
|
82 | type ProjectPaths = Paths<Project>;
|
83 | //=> 'filename' | 'listA' | 'listB' | 'folder' | `listA.${number}` | 'listB.0' | 'listB.0.filename' | 'folder.subfolder' | 'folder.subfolder.filename'
|
84 |
|
85 | declare function open<Path extends ProjectPaths>(path: Path): void;
|
86 |
|
87 | open('filename'); // Pass
|
88 | open('folder.subfolder'); // Pass
|
89 | open('folder.subfolder.filename'); // Pass
|
90 | open('foo'); // TypeError
|
91 |
|
92 | // Also works with arrays
|
93 | open('listA.1'); // Pass
|
94 | open('listB.0'); // Pass
|
95 | open('listB.1'); // TypeError. Because listB only has one element.
|
96 | ```
|
97 |
|
98 | @category Object
|
99 | @category Array
|
100 | */
|
101 | export 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 |
|
108 | type _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 |
|
123 | type 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;
|