1 | import type {StringDigit, ToString} from './internal';
|
2 | import type {LiteralStringUnion} from './literal-union';
|
3 | import type {Paths} from './paths';
|
4 | import type {Split} from './split';
|
5 | import type {StringKeyOf} from './string-key-of';
|
6 |
|
7 | type GetOptions = {
|
8 | |
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | strict?: boolean;
|
16 | };
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | type GetWithPath<BaseType, Keys, Options extends GetOptions = {}> =
|
22 | Keys extends readonly []
|
23 | ? BaseType
|
24 | : Keys extends readonly [infer Head, ...infer Tail]
|
25 | ? GetWithPath<
|
26 | PropertyOf<BaseType, Extract<Head, string>, Options>,
|
27 | Extract<Tail, string[]>,
|
28 | Options
|
29 | >
|
30 | : never;
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | type Strictify<Type, Options extends GetOptions> =
|
36 | Options['strict'] extends false ? Type : (Type | undefined);
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | type StrictPropertyOf<BaseType, Key extends keyof BaseType, Options extends GetOptions> =
|
45 | Record<string, any> extends BaseType
|
46 | ? string extends keyof BaseType
|
47 | ? Strictify<BaseType[Key], Options>
|
48 | : BaseType[Key]
|
49 | : BaseType[Key];
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | type ToPath<S extends string> = Split<FixPathSquareBrackets<S>, '.'>;
|
64 |
|
65 |
|
66 |
|
67 |
|
68 | type FixPathSquareBrackets<Path extends string> =
|
69 | Path extends `[${infer Head}]${infer Tail}`
|
70 | ? Tail extends `[${string}`
|
71 | ? `${Head}.${FixPathSquareBrackets<Tail>}`
|
72 | : `${Head}${FixPathSquareBrackets<Tail>}`
|
73 | : Path extends `${infer Head}[${infer Middle}]${infer Tail}`
|
74 | ? `${Head}.${FixPathSquareBrackets<`[${Middle}]${Tail}`>}`
|
75 | : Path;
|
76 |
|
77 | /**
|
78 | Returns true if `LongString` is made up out of `Substring` repeated 0 or more times.
|
79 |
|
80 | @example
|
81 | ```
|
82 | ConsistsOnlyOf<'aaa', 'a'>
|
83 | ConsistsOnlyOf<'ababab', 'ab'>
|
84 | ConsistsOnlyOf<'aBa', 'a'>
|
85 | ConsistsOnlyOf<'', 'a'>
|
86 | ```
|
87 | */
|
88 | type ConsistsOnlyOf<LongString extends string, Substring extends string> =
|
89 | LongString extends ''
|
90 | ? true
|
91 | : LongString extends `${Substring}${infer Tail}`
|
92 | ? ConsistsOnlyOf<Tail, Substring>
|
93 | : false;
|
94 |
|
95 | /**
|
96 | Convert a type which may have number keys to one with string keys, making it possible to index using strings retrieved from template types.
|
97 |
|
98 | @example
|
99 | ```
|
100 | type WithNumbers = {foo: string; 0: boolean};
|
101 | type WithStrings = WithStringKeys<WithNumbers>;
|
102 |
|
103 | type WithNumbersKeys = keyof WithNumbers;
|
104 |
|
105 | type WithStringsKeys = keyof WithStrings;
|
106 |
|
107 | ```
|
108 | */
|
109 | type WithStringKeys<BaseType> = {
|
110 | [Key in StringKeyOf<BaseType>]: UncheckedIndex<BaseType, Key>
|
111 | };
|
112 |
|
113 | /**
|
114 | Perform a `T[U]` operation if `T` supports indexing.
|
115 | */
|
116 | type UncheckedIndex<T, U extends string | number> = [T] extends [Record<string | number, any>] ? T[U] : never;
|
117 |
|
118 | /**
|
119 | Get a property of an object or array. Works when indexing arrays using number-literal-strings, for example, `PropertyOf<number[], '0'> = number`, and when indexing objects with number keys.
|
120 |
|
121 | Note:
|
122 | - Returns `unknown` if `Key` is not a property of `BaseType`, since TypeScript uses structural typing, and it cannot be guaranteed that extra properties unknown to the type system will exist at runtime.
|
123 | - Returns `undefined` from nullish values, to match the behaviour of most deep-key libraries like `lodash`, `dot-prop`, etc.
|
124 | */
|
125 | type PropertyOf<BaseType, Key extends string, Options extends GetOptions = {}> =
|
126 | BaseType extends null | undefined
|
127 | ? undefined
|
128 | : Key extends keyof BaseType
|
129 | ? StrictPropertyOf<BaseType, Key, Options>
|
130 | : BaseType extends readonly [] | readonly [unknown, ...unknown[]]
|
131 | ? unknown // It's a tuple, but `Key` did not extend `keyof BaseType`. So the index is out of bounds.
|
132 | : BaseType extends {
|
133 | [n: number]: infer Item;
|
134 | length: number; // Note: This is needed to avoid being too lax with records types using number keys like `{0: string; 1: boolean}`.
|
135 | }
|
136 | ? (
|
137 | ConsistsOnlyOf<Key, StringDigit> extends true
|
138 | ? Strictify<Item, Options>
|
139 | : unknown
|
140 | )
|
141 | : Key extends keyof WithStringKeys<BaseType>
|
142 | ? StrictPropertyOf<WithStringKeys<BaseType>, Key, Options>
|
143 | : unknown;
|
144 |
|
145 | // This works by first splitting the path based on `.` and `[...]` characters into a tuple of string keys. Then it recursively uses the head key to get the next property of the current object, until there are no keys left. Number keys extract the item type from arrays, or are converted to strings to extract types from tuples and dictionaries with number keys.
|
146 | /**
|
147 | Get a deeply-nested property from an object using a key path, like Lodash's `.get()` function.
|
148 |
|
149 | Use-case: Retrieve a property from deep inside an API response or some other complex object.
|
150 |
|
151 | @example
|
152 | ```
|
153 | import type {Get} from 'type-fest';
|
154 | import * as lodash from 'lodash';
|
155 |
|
156 | const get = <BaseType, Path extends string | readonly string[]>(object: BaseType, path: Path): Get<BaseType, Path> =>
|
157 | lodash.get(object, path);
|
158 |
|
159 | interface ApiResponse {
|
160 | hits: {
|
161 | hits: Array<{
|
162 | _id: string
|
163 | _source: {
|
164 | name: Array<{
|
165 | given: string[]
|
166 | family: string
|
167 | }>
|
168 | birthDate: string
|
169 | }
|
170 | }>
|
171 | }
|
172 | }
|
173 |
|
174 | const getName = (apiResponse: ApiResponse) =>
|
175 | get(apiResponse, 'hits.hits[0]._source.name');
|
176 |
|
177 |
|
178 |
|
179 | const getNameWithPathArray = (apiResponse: ApiResponse) =>
|
180 | get(apiResponse, ['hits','hits', '0', '_source', 'name'] as const);
|
181 |
|
182 |
|
183 |
|
184 | Get<string[], '3', {strict: false}>
|
185 | Get<Record<string, string>, 'foo', {strict: true}>
|
186 | ```
|
187 |
|
188 | @category Object
|
189 | @category Array
|
190 | @category Template literal
|
191 | */
|
192 | export type Get<
|
193 | BaseType,
|
194 | Path extends
|
195 | | readonly string[]
|
196 | | LiteralStringUnion<ToString<Paths<BaseType, {bracketNotation: false}> | Paths<BaseType, {bracketNotation: true}>>>,
|
197 | Options extends GetOptions = {}> =
|
198 | GetWithPath<BaseType, Path extends string ? ToPath<Path> : Path, Options>;
|
199 |
|
\ | No newline at end of file |