UNPKG

6.71 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 : 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/**
147Get a deeply-nested property from an object using a key path, like Lodash's `.get()` function.
148
149Use-case: Retrieve a property from deep inside an API response or some other complex object.
150
151@example
152```
153import type {Get} from 'type-fest';
154import * as lodash from 'lodash';
155
156const get = <BaseType, Path extends string | readonly string[]>(object: BaseType, path: Path): Get<BaseType, Path> =>
157 lodash.get(object, path);
158
159interface 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
174const getName = (apiResponse: ApiResponse) =>
175 get(apiResponse, 'hits.hits[0]._source.name');
176 //=> Array<{given: string[]; family: string}> | undefined
177
178// Path also supports a readonly array of strings
179const getNameWithPathArray = (apiResponse: ApiResponse) =>
180 get(apiResponse, ['hits','hits', '0', '_source', 'name'] as const);
181 //=> Array<{given: string[]; family: string}> | undefined
182
183// Non-strict mode:
184Get<string[], '3', {strict: false}> //=> string
185Get<Record<string, string>, 'foo', {strict: true}> // => string
186```
187
188@category Object
189@category Array
190@category Template literal
191*/
192export 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