Skip to content

Commit

Permalink
Deep property get
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Nov 25, 2020
1 parent 951a211 commit e65fbdb
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 10 deletions.
8 changes: 8 additions & 0 deletions 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<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
[S];
2 changes: 2 additions & 0 deletions 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';
3 changes: 2 additions & 1 deletion 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
Expand Down
50 changes: 50 additions & 0 deletions 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<Get<ApiResponse, 'hits.hits[0]._source.name'>>([{given: ['Homer', 'J'], family: 'Simpson'}]);
expectType<Get<ApiResponse, 'hits.hits.0._source.name'>>([{given: ['Homer', 'J'], family: 'Simpson'}]);
expectType<Get<ApiResponse, 'hits.hits[12345]._source.name'>>([{given: ['Homer', 'J'], family: 'Simpson'}]);

expectType<Get<ApiResponse, 'hits.someNonsense.notTheRightPath'>>({} as never);

interface WithTuples {
foo: [
{bar: number},
{baz: boolean}
];
}

expectType<Get<WithTuples, 'foo[0].bar'>>(123);
expectType<Get<WithTuples, 'foo.0.bar'>>(123);

expectType<Get<WithTuples, 'foo[1].bar'>>({} as never);
expectType<Get<WithTuples, 'foo.1.bar'>>({} as never);

interface WithNumberKeys {
foo: {
1: {
bar: number;
};
};
}

expectType<Get<WithNumberKeys, 'foo[1].bar'>>(123);
expectType<Get<WithNumberKeys, 'foo.1.bar'>>(123);

expectType<Get<WithNumberKeys, 'foo[2].bar'>>({} as never);
expectType<Get<WithNumberKeys, 'foo.2.bar'>>({} as never);
10 changes: 1 addition & 9 deletions 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<S extends string, D extends string> =
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
[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.
Expand Down
82 changes: 82 additions & 0 deletions 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<ApiResponse, 'hits.hits[0]._source.name'> // 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<Object, Path extends string> = GetWithPath<Object, ToPath<Path>>;

/**
* Like @see Get but receives an array of strings as a path parameter.
*/
type GetWithPath<T, Keys extends string[]> =
Keys extends []
? T
: Keys extends [infer Head, ...infer Tail]
? GetWithPath<PropertyOf<T, Extract<Head, string>>, Extract<Tail, string[]>>
: never;

type ToPath<S extends string> = Split<FixPathSquareBrackets<S>, '.'>;

type FixPathSquareBrackets<S extends string> =
S extends `${infer T}[${infer U}]${infer V}`
? `${T}.${U}${FixPathSquareBrackets<V>}`
: S;

type ConsistsOnlyOf<S extends string, C extends string> =
S extends `${C}`
? true
: S extends `${C}${infer Tail}`
? ConsistsOnlyOf<Tail, C>
: false;

type IsInteger<S extends string> = ConsistsOnlyOf<S, Integers>;

type WithStringKeys<T extends Record<string | number, any>> = {
[K in `${Extract<keyof T, string | number>}`]: T[K]
};

/**
* Get a property of an object or array. Works when indexing arrays using number-literal-strings, e.g. `PropertyOf<number[], '0'> = number`,
* and when indexing objects with number keys.
* Returns `never` if `Key` is not a property of `Object`,
*/
type PropertyOf<Object, Key extends string> =
Key extends keyof Object
? Object[Key]
: Object extends Array<infer Item>
? IsInteger<Key> extends true
? Item
: never
: Key extends keyof WithStringKeys<Object>
? WithStringKeys<Object>[Key]
: never;

0 comments on commit e65fbdb

Please sign in to comment.