UNPKG

7.08 kBTypeScriptView Raw
1import type {StringDigit, ToString} from './internal';
2import type {LiteralStringUnion} from './literal-union';
3import type {Paths} from './paths';
4import type {Split} from './split';
5import type {StringKeyOf} from './string-key-of';
6
7type GetOptions = {
8 /**
9 Include `undefined` in the return type when accessing properties.
10
11 Setting this to `false` is not recommended.
12
13 @default true
14 */
15 strict?: boolean;
16};
17
18/**
19Like the `Get` type but receives an array of strings as a path parameter.
20*/
21type 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/**
33Adds `undefined` to `Type` if `strict` is enabled.
34*/
35type Strictify<Type, Options extends GetOptions> =
36 Options['strict'] extends false ? Type : (Type | undefined);
37
38/**
39If `Options['strict']` is `true`, includes `undefined` in the returned type when accessing properties on `Record<string, any>`.
40
41Known limitations:
42- Does not include `undefined` in the type on object types with an index signature (for example, `{a: string; [key: string]: string}`).
43*/
44type 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> // Record<string, any>
48 : BaseType[Key] // Record<'a' | 'b', any> (Records with a string union as keys have required properties)
49 : BaseType[Key];
50
51/**
52Splits a dot-prop style path into a tuple comprised of the properties in the path. Handles square-bracket notation.
53
54@example
55```
56ToPath<'foo.bar.baz'>
57//=> ['foo', 'bar', 'baz']
58
59ToPath<'foo[0].bar.baz'>
60//=> ['foo', '0', 'bar', 'baz']
61```
62*/
63type ToPath<S extends string> = Split<FixPathSquareBrackets<S>, '.'>;
64
65/**
66Replaces square-bracketed dot notation with dots, for example, `foo[0].bar` -> `foo.0.bar`.
67*/
68type 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/**
78Returns true if `LongString` is made up out of `Substring` repeated 0 or more times.
79
80@example
81```
82ConsistsOnlyOf<'aaa', 'a'> //=> true
83ConsistsOnlyOf<'ababab', 'ab'> //=> true
84ConsistsOnlyOf<'aBa', 'a'> //=> false
85ConsistsOnlyOf<'', 'a'> //=> true
86```
87*/
88type 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/**
96Convert 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```
100type WithNumbers = {foo: string; 0: boolean};
101type WithStrings = WithStringKeys<WithNumbers>;
102
103type WithNumbersKeys = keyof WithNumbers;
104//=> 'foo' | 0
105type WithStringsKeys = keyof WithStrings;
106//=> 'foo' | '0'
107```
108*/
109type WithStringKeys<BaseType> = {
110 [Key in StringKeyOf<BaseType>]: UncheckedIndex<BaseType, Key>
111};
112
113/**
114Perform a `T[U]` operation if `T` supports indexing.
115*/
116type UncheckedIndex<T, U extends string | number> = [T] extends [Record<string | number, any>] ? T[U] : never;
117
118/**
119Get 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
121Note:
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*/
125type 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 // Handle arrays and tuples
131 : BaseType extends readonly unknown[]
132 ? Key extends `${number}`
133 // For arrays with unknown length (regular arrays)
134 ? number extends BaseType['length']
135 ? Strictify<BaseType[number], Options>
136 // For tuples: check if the index is valid
137 : Key extends keyof BaseType
138 ? Strictify<BaseType[Key & keyof BaseType], Options>
139 // Out-of-bounds access for tuples
140 : unknown
141 // Non-numeric string key for arrays/tuples
142 : unknown
143 // Handle array-like objects
144 : BaseType extends {
145 [n: number]: infer Item;
146 length: number; // Note: This is needed to avoid being too lax with records types using number keys like `{0: string; 1: boolean}`.
147 }
148 ? (
149 ConsistsOnlyOf<Key, StringDigit> extends true
150 ? Strictify<Item, Options>
151 : unknown
152 )
153 : Key extends keyof WithStringKeys<BaseType>
154 ? StrictPropertyOf<WithStringKeys<BaseType>, Key, Options>
155 : unknown;
156
157// 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.
158/**
159Get a deeply-nested property from an object using a key path, like Lodash's `.get()` function.
160
161Use-case: Retrieve a property from deep inside an API response or some other complex object.
162
163@example
164```
165import type {Get} from 'type-fest';
166import * as lodash from 'lodash';
167
168const get = <BaseType, Path extends string | readonly string[]>(object: BaseType, path: Path): Get<BaseType, Path> =>
169 lodash.get(object, path);
170
171interface ApiResponse {
172 hits: {
173 hits: Array<{
174 _id: string
175 _source: {
176 name: Array<{
177 given: string[]
178 family: string
179 }>
180 birthDate: string
181 }
182 }>
183 }
184}
185
186const getName = (apiResponse: ApiResponse) =>
187 get(apiResponse, 'hits.hits[0]._source.name');
188 //=> Array<{given: string[]; family: string}> | undefined
189
190// Path also supports a readonly array of strings
191const getNameWithPathArray = (apiResponse: ApiResponse) =>
192 get(apiResponse, ['hits','hits', '0', '_source', 'name'] as const);
193 //=> Array<{given: string[]; family: string}> | undefined
194
195// Non-strict mode:
196Get<string[], '3', {strict: false}> //=> string
197Get<Record<string, string>, 'foo', {strict: true}> // => string
198```
199
200@category Object
201@category Array
202@category Template literal
203*/
204export type Get<
205 BaseType,
206 Path extends
207 | readonly string[]
208 | LiteralStringUnion<ToString<Paths<BaseType, {bracketNotation: false}> | Paths<BaseType, {bracketNotation: true}>>>,
209 Options extends GetOptions = {}> =
210 GetWithPath<BaseType, Path extends string ? ToPath<Path> : Path, Options>;
211
\No newline at end of file