|
| 1 | +import type {EmptyObject} from './empty-object'; |
| 2 | +import type {IsAny} from './is-any'; |
| 3 | +import type {IsNever} from './is-never'; |
| 4 | +import type {UnknownArray} from './unknown-array'; |
| 5 | +import type {UnknownRecord} from './unknown-record'; |
| 6 | + |
| 7 | +type ToString<T> = T extends string | number ? `${T}` : never; |
| 8 | + |
| 9 | +/** |
| 10 | +Return the part of the given array with a fixed index. |
| 11 | +
|
| 12 | +@example |
| 13 | +``` |
| 14 | +type A = [string, number, boolean, ...string[]]; |
| 15 | +type B = FilterFixedIndexArray<A>; |
| 16 | +//=> [string, number, boolean] |
| 17 | +``` |
| 18 | +*/ |
| 19 | +type FilterFixedIndexArray<T extends UnknownArray, Result extends UnknownArray = []> = |
| 20 | + number extends T['length'] ? |
| 21 | + T extends readonly [infer U, ...infer V] |
| 22 | + ? FilterFixedIndexArray<V, [...Result, U]> |
| 23 | + : Result |
| 24 | + : T; |
| 25 | + |
| 26 | +/** |
| 27 | +Return the part of the given array with a non-fixed index. |
| 28 | +
|
| 29 | +@example |
| 30 | +``` |
| 31 | +type A = [string, number, boolean, ...string[]]; |
| 32 | +type B = FilterNotFixedIndexArray<A>; |
| 33 | +//=> string[] |
| 34 | +``` |
| 35 | +*/ |
| 36 | +type FilterNotFixedIndexArray<T extends UnknownArray> = |
| 37 | +T extends readonly [...FilterFixedIndexArray<T>, ...infer U] |
| 38 | + ? U |
| 39 | + : []; |
| 40 | + |
| 41 | +/** |
| 42 | +Generate a union of all possible paths to properties in the given object. |
| 43 | +
|
| 44 | +It also works with arrays. |
| 45 | +
|
| 46 | +Use-case: You want a type-safe way to access deeply nested properties in an object. |
| 47 | +
|
| 48 | +@example |
| 49 | +``` |
| 50 | +import type {Paths} from 'type-fest'; |
| 51 | +
|
| 52 | +type Project = { |
| 53 | + filename: string; |
| 54 | + listA: string[]; |
| 55 | + listB: [{filename: string}]; |
| 56 | + folder: { |
| 57 | + subfolder: { |
| 58 | + filename: string; |
| 59 | + }; |
| 60 | + }; |
| 61 | +}; |
| 62 | +
|
| 63 | +type ProjectPaths = Paths<Project>; |
| 64 | +//=> 'filename' | 'listA' | 'listB' | 'folder' | `listA.${number}` | 'listB.0' | 'listB.0.filename' | 'folder.subfolder' | 'folder.subfolder.filename' |
| 65 | +
|
| 66 | +declare function open<Path extends ProjectPaths>(path: Path): void; |
| 67 | +
|
| 68 | +open('filename'); // Pass |
| 69 | +open('folder.subfolder'); // Pass |
| 70 | +open('folder.subfolder.filename'); // Pass |
| 71 | +open('foo'); // TypeError |
| 72 | +
|
| 73 | +// Also works with arrays |
| 74 | +open('listA.1'); // Pass |
| 75 | +open('listB.0'); // Pass |
| 76 | +open('listB.1'); // TypeError. Because listB only has one element. |
| 77 | +``` |
| 78 | +
|
| 79 | +@category Object |
| 80 | +@category Array |
| 81 | +*/ |
| 82 | +export type Paths<T extends UnknownRecord | UnknownArray> = |
| 83 | + IsAny<T> extends true |
| 84 | + ? never |
| 85 | + : T extends UnknownArray |
| 86 | + ? number extends T['length'] |
| 87 | + // We need to handle the fixed and non-fixed index part of the array separately. |
| 88 | + ? InternalPaths<FilterFixedIndexArray<T>> |
| 89 | + | InternalPaths<Array<FilterNotFixedIndexArray<T>[number]>> |
| 90 | + : InternalPaths<T> |
| 91 | + : InternalPaths<T>; |
| 92 | + |
| 93 | +export type InternalPaths<_T extends UnknownRecord | UnknownArray, T = Required<_T>> = |
| 94 | + T extends EmptyObject | readonly [] |
| 95 | + ? never |
| 96 | + : { |
| 97 | + [Key in keyof T]: |
| 98 | + Key extends string | number // Limit `Key` to string or number. |
| 99 | + ? T[Key] extends UnknownRecord | UnknownArray |
| 100 | + ? ( |
| 101 | + IsNever<Paths<T[Key]>> extends false |
| 102 | + // If `Key` is a number, return `Key | `${Key}``, because both `array[0]` and `array['0']` do not work. |
| 103 | + ? Key | ToString<Key> | `${Key}.${Paths<T[Key]>}` |
| 104 | + : Key | ToString<Key> |
| 105 | + ) |
| 106 | + : Key | ToString<Key> |
| 107 | + : never |
| 108 | + }[keyof T & (T extends UnknownArray ? number : unknown)]; |
0 commit comments