Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Get type #153

Merged
merged 30 commits into from Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1f64736
Deep property get
mmkal Nov 25, 2020
0109858
move split to ts41
mmkal Nov 26, 2020
81e69f4
Move split to utilities file
mmkal Nov 26, 2020
59e1548
Improve docs/naming
mmkal Nov 26, 2020
9b76341
Return undefined for bad paths, not never
mmkal Nov 26, 2020
49c60e5
use expect-type for clearer assertions
mmkal Nov 26, 2020
859456a
Update get.ts
sindresorhus Nov 27, 2020
874ced0
Update get.d.ts
sindresorhus Nov 27, 2020
54e97b5
Update get.d.ts
sindresorhus Nov 27, 2020
87974e1
Update get.d.ts
sindresorhus Nov 27, 2020
8cae0d5
Add some tests; return unknown for bad paths
mmkal Nov 28, 2020
74571d1
Rename Integers -> StringDigit and add JSDoc
mmkal Nov 29, 2020
483116f
Handle optional props
mmkal Nov 29, 2020
0804a75
Tab indentation
mmkal Nov 29, 2020
d9a64a9
Use words as type names
mmkal Nov 30, 2020
8ee134f
Make api response test more realistic
mmkal Nov 30, 2020
d971417
Update get.d.ts
sindresorhus Dec 1, 2020
8476a89
Fix @see tag
mmkal Dec 3, 2020
12a8978
Merge branch 'get' of https://github.com/mmkal/type-fest into get
mmkal Dec 3, 2020
498cea8
Merge remote-tracking branch 'upstream/master' into get
mmkal Dec 3, 2020
6078ae9
ObjectType -> BaseType
mmkal Dec 3, 2020
54eb532
Improve ConsistsOnlyOf docs
mmkal Dec 3, 2020
84529f7
Add to PropertyOf docs
mmkal Dec 3, 2020
fff3b74
Don't use ArrayLike
mmkal Dec 3, 2020
3f0f49f
Update get.ts
sindresorhus Dec 20, 2020
cbde1ca
Update get.d.ts
sindresorhus Dec 20, 2020
d621f86
Merge remote-tracking branch 'upstream/master' into get
mmkal Jan 2, 2021
35ecd37
Bump expect-type
mmkal Jan 2, 2021
6560c29
Update @see comment
mmkal Jan 2, 2021
4e3d34a
See no evil
mmkal Jan 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -36,6 +36,7 @@
],
"devDependencies": {
"@sindresorhus/tsconfig": "~0.7.0",
"expect-type": "^0.9.0",
mmkal marked this conversation as resolved.
Show resolved Hide resolved
"tsd": "^0.13.1",
"typescript": "^4.1.2",
"xo": "^0.35.0"
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -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) - Get a deeply-nested property from an object using a key path, like Lodash's `.get()` function.
mmkal marked this conversation as resolved.
Show resolved Hide resolved

### Miscellaneous

Expand Down
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';
mmkal marked this conversation as resolved.
Show resolved Hide resolved
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 '../ts41/utilities';
import {expectType, expectAssignable} from 'tsd';

// Split
Expand Down
49 changes: 49 additions & 0 deletions test-d/get.ts
@@ -0,0 +1,49 @@
import {Get} from '../ts41/get';
import {expectTypeOf} from 'expect-type';

interface ApiResponse {
hits: {
hits: Array<{
_id: string;
mmkal marked this conversation as resolved.
Show resolved Hide resolved
_source: {
name: Array<{
mmkal marked this conversation as resolved.
Show resolved Hide resolved
given: string[];
family: string;
}>;
birthDate: string;
};
}>;
};
}

expectTypeOf<Get<ApiResponse, 'hits.hits[0]._source.name'>>().toEqualTypeOf<Array<{given: string[]; family: string}>>();
expectTypeOf<Get<ApiResponse, 'hits.hits.0._source.name'>>().toEqualTypeOf<Array<{given: string[]; family: string}>>();

expectTypeOf<Get<ApiResponse, 'hits.someNonsense.notTheRightPath'>>().toBeUndefined();

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

expectTypeOf<Get<WithTuples, 'foo[0].bar'>>().toBeNumber();
expectTypeOf<Get<WithTuples, 'foo.0.bar'>>().toBeNumber();

expectTypeOf<Get<WithTuples, 'foo[1].bar'>>().toBeUndefined();
expectTypeOf<Get<WithTuples, 'foo.1.bar'>>().toBeUndefined();

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

expectTypeOf<Get<WithNumberKeys, 'foo[1].bar'>>().toBeNumber();
expectTypeOf<Get<WithNumberKeys, 'foo.1.bar'>>().toBeNumber();

expectTypeOf<Get<WithNumberKeys, 'foo[2].bar'>>().toBeUndefined();
expectTypeOf<Get<WithNumberKeys, 'foo.2.bar'>>().toBeUndefined();
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 './utilities';

/**
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
95 changes: 95 additions & 0 deletions ts41/get.d.ts
@@ -0,0 +1,95 @@
import {Split} from './utilities';
import {Integers} from '../source/utilities';

/**
Like @see Get but receives an array of strings as a path parameter.
mmkal marked this conversation as resolved.
Show resolved Hide resolved
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
*/
type GetWithPath<T, Keys extends readonly 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>, '.'>;
mmkal marked this conversation as resolved.
Show resolved Hide resolved

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> =
mmkal marked this conversation as resolved.
Show resolved Hide resolved
S extends ''
? true
: S extends `${C}${infer Tail}`
? ConsistsOnlyOf<Tail, C>
: false;

type IsInteger<S extends string> = ConsistsOnlyOf<S, Integers>;
mmkal marked this conversation as resolved.
Show resolved Hide resolved

/**
Convert a type which may have number keys to one with string keys, making it possible to index
using strings retrieved from template types.

@example
```
type WithNumbers = { foo: string; 0: boolean }
type WithStrings = WithStringKeys<WithNumbers>

type WithNumbersKeys = keyof WithNumbers // evaluates to 'foo' | 0
type WithStringsKeys = keyof WithStrings // evaluates to 'foo' | '0'
```
*/
type WithStringKeys<T extends Record<string | number, any>> = {
mmkal marked this conversation as resolved.
Show resolved Hide resolved
[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 `neundefinedver` if `Key` is not a property of `Object`,
mmkal marked this conversation as resolved.
Show resolved Hide resolved
*/
type PropertyOf<ObjectType, Key extends string> =
Key extends keyof ObjectType
? ObjectType[Key]
: ObjectType extends Array<infer Item>
? IsInteger<Key> extends true
? Item
: undefined
: Key extends keyof WithStringKeys<ObjectType>
? WithStringKeys<ObjectType>[Key]
: undefined;

/**
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 }>
```

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>>;
mmkal marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions ts41/utilities.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> =
mmkal marked this conversation as resolved.
Show resolved Hide resolved
string extends S ? string[] :
S extends '' ? [] :
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] :
[S];