diff --git a/readme.md b/readme.md index 714df7868..9f6961c17 100644 --- a/readme.md +++ b/readme.md @@ -97,6 +97,7 @@ Click the type names for complete docs. - [`PascalCase`](ts41/pascal-case.d.ts) – Converts a string literal to pascal-case (`FooBar`) - [`SnakeCase`](ts41/snake-case.d.ts) – Convert a string literal to snake-case (`foo_bar`). - [`DelimiterCase`](ts41/delimiter-case.d.ts) – Convert a string literal to a custom string delimiter casing. +- [`Get`](ts41/get.d.ts) - Gets a deeply-nested property from an object, like lodash's get method. ### Miscellaneous diff --git a/source/split.d.ts b/source/split.d.ts new file mode 100644 index 000000000..f82e362fb --- /dev/null +++ b/source/split.d.ts @@ -0,0 +1,8 @@ +/** +Recursively split a string literal into two parts on the first occurence of the given string, returning an array literal of all the separate parts. +*/ +export type Split = + string extends S ? string[] : + S extends '' ? [] : + S extends `${infer T}${D}${infer U}` ? [T, ...Split] : + [S]; diff --git a/source/utilities.d.ts b/source/utilities.d.ts index c079b06ff..934e0e571 100644 --- a/source/utilities.d.ts +++ b/source/utilities.d.ts @@ -1,3 +1,5 @@ export type UpperCaseCharacters = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'X' | 'Y' | 'Z'; export type WordSeparators = '-' | '_' | ' '; + +export type Integers = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; diff --git a/test-d/camel-case.ts b/test-d/camel-case.ts index 7c4720673..b6a5a8a9f 100644 --- a/test-d/camel-case.ts +++ b/test-d/camel-case.ts @@ -1,4 +1,5 @@ -import {Split, CamelCase} from '../source/camel-case'; +import {CamelCase} from '../source/camel-case'; +import {Split} from '../source/split'; import {expectType, expectAssignable} from 'tsd'; // Split diff --git a/test-d/get.ts b/test-d/get.ts new file mode 100644 index 000000000..8984cccef --- /dev/null +++ b/test-d/get.ts @@ -0,0 +1,50 @@ +import {Get} from '../ts41/get'; +import {expectType} from 'tsd'; + +interface ApiResponse { + hits: { + hits: Array<{ + _id: string; + _source: { + name: Array<{ + given: string[]; + family: string; + }>; + birthDate: string; + }; + }>; + }; +} + +expectType>([{given: ['Homer', 'J'], family: 'Simpson'}]); +expectType>([{given: ['Homer', 'J'], family: 'Simpson'}]); +expectType>([{given: ['Homer', 'J'], family: 'Simpson'}]); + +expectType>({} as never); + +interface WithTuples { + foo: [ + {bar: number}, + {baz: boolean} + ]; +} + +expectType>(123); +expectType>(123); + +expectType>({} as never); +expectType>({} as never); + +interface WithNumberKeys { + foo: { + 1: { + bar: number; + }; + }; +} + +expectType>(123); +expectType>(123); + +expectType>({} as never); +expectType>({} as never); diff --git a/ts41/camel-case.d.ts b/ts41/camel-case.d.ts index 4476fd302..f3c485cd3 100644 --- a/ts41/camel-case.d.ts +++ b/ts41/camel-case.d.ts @@ -1,13 +1,5 @@ import {WordSeparators} from '../source/utilities'; - -/** -Recursively split a string literal into two parts on the first occurence of the given string, returning an array literal of all the separate parts. -*/ -export type Split = - string extends S ? string[] : - S extends '' ? [] : - S extends `${infer T}${D}${infer U}` ? [T, ...Split] : - [S]; +import {Split} from '../source/split'; /** Step by step takes the first item in an array literal, formats it and adds it to a string literal, and then recursively appends the remainder. diff --git a/ts41/get.d.ts b/ts41/get.d.ts new file mode 100644 index 000000000..47e94d200 --- /dev/null +++ b/ts41/get.d.ts @@ -0,0 +1,82 @@ +import {Split} from '../source/split'; +import {Integers} from '../source/utilities'; + +/** + * Gets a deeply-nested property from an object, like lodash's `get` method. + * + * Use-case: retrieve a property from deep inside an API response or other complex object. + * + * @example + * import { Get } from 'type-fest' + * + * interface ApiResponse { + * hits: { + * hits: Array<{ + * _id: string + * _source: { + * name: Array<{ + * given: string[] + * family: string + * }> + * birthDate: string + * } + * }> + * } + * } + * + * type Name = Get // Array<{ given: string[]; family: string }> + * + * @explanation + * + * 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. + */ +export type Get = GetWithPath>; + +/** + * Like @see Get but receives an array of strings as a path parameter. + */ +type GetWithPath = + Keys extends [] + ? T + : Keys extends [infer Head, ...infer Tail] + ? GetWithPath>, Extract> + : never; + +type ToPath = Split, '.'>; + +type FixPathSquareBrackets = + S extends `${infer T}[${infer U}]${infer V}` + ? `${T}.${U}${FixPathSquareBrackets}` + : S; + +type ConsistsOnlyOf = + S extends '' + ? true + : S extends `${C}${infer Tail}` + ? ConsistsOnlyOf + : false; + +type IsInteger = ConsistsOnlyOf; + +type WithStringKeys> = { + [K in `${Extract}`]: T[K] +}; + +/** + * Get a property of an object or array. Works when indexing arrays using number-literal-strings, e.g. `PropertyOf = number`, + * and when indexing objects with number keys. + * Returns `never` if `Key` is not a property of `Object`, + */ +type PropertyOf = + Key extends keyof Object + ? Object[Key] + : Object extends Array + ? IsInteger extends true + ? Item + : never + : Key extends keyof WithStringKeys + ? WithStringKeys[Key] + : never;