UNPKG

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