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 25 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 @@ -35,6 +35,7 @@
],
"devDependencies": {
"@sindresorhus/tsconfig": "~0.7.0",
"expect-type": "^0.9.0",
mmkal marked this conversation as resolved.
Show resolved Hide resolved
"tsd": "^0.14.0",
"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()`](https://lodash.com/docs/latest#get) function.

### 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' | 'W' | 'X' | 'Y' | 'Z';

export type WordSeparators = '-' | '_' | ' ';

export type StringDigit = '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 '../ts41/camel-case';
import {CamelCase} from '../ts41/camel-case';
import {Split} from '../ts41/utilities';
import {expectType, expectAssignable} from 'tsd';

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

declare const get: <ObjectType, Path extends string>(object: ObjectType, path: Path) => Get<ObjectType, Path>;

interface ApiResponse {
hits: {
hits: Array<{
_id: string;
_source: {
name: Array<{
given: string[];
family: string;
}>;
birthDate: string;
};
}>;
};
}

declare const apiResponse: ApiResponse;

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.hits[0]._source.name[0].given[0]')).toBeString();

// TypeScript is structurally typed. It's *possible* this value exists even though it's not on the parent interface, so the type is `unknown`.
expectTypeOf(get(apiResponse, 'hits.someNonsense.notTheRightPath')).toBeUnknown();

// This interface uses a tuple type (as opposed to an array).
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].baz'>>().toBeBoolean();
expectTypeOf<Get<WithTuples, 'foo[1].bar'>>().toBeUnknown();

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'>>().toBeUnknown();
expectTypeOf<Get<WithNumberKeys, 'foo.2.bar'>>().toBeUnknown();

// Test `readonly`, `ReadonlyArray`, optional properties, and unions with null.

interface WithModifiers {
foo: ReadonlyArray<{
bar?: {
readonly baz: {
qux: number;
};
};
abc: {
def: {
ghi: string;
};
} | null;
}>;
}

expectTypeOf<Get<WithModifiers, 'foo[0].bar.baz'>>().toEqualTypeOf<{qux: number} | undefined>();
expectTypeOf<Get<WithModifiers, 'foo[0].abc.def.ghi'>>().toEqualTypeOf<string | undefined>();
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
131 changes: 131 additions & 0 deletions ts41/get.d.ts
@@ -0,0 +1,131 @@
import {Split} from './utilities';
import {StringDigit} from '../source/utilities';

/**
Like @see {@link Get} but receives an array of strings as a path parameter.
mmkal marked this conversation as resolved.
Show resolved Hide resolved
*/
type GetWithPath<BaseType, Keys extends readonly string[]> =
Keys extends []
? BaseType
: Keys extends [infer Head, ...infer Tail]
? GetWithPath<PropertyOf<BaseType, Extract<Head, string>>, Extract<Tail, string[]>>
: never;

/**
Splits a dot-prop style path into a tuple comprised of the properties in the path. Handles square-bracket notation.

@example
```
ToPath<'foo.bar.baz'>
//=> ['foo', 'bar', 'baz']

ToPath<'foo[0].bar.baz'>
//=> ['foo', '0', 'bar', 'baz']
```
*/
type ToPath<S extends string> = Split<FixPathSquareBrackets<S>, '.'>;
mmkal marked this conversation as resolved.
Show resolved Hide resolved

/**
Replaces square-bracketed dot notation with dots, for example, `foo[0].bar` -> `foo.0.bar`.
*/
type FixPathSquareBrackets<Path extends string> =
Path extends `${infer Head}[${infer Middle}]${infer Tail}`
? `${Head}.${Middle}${FixPathSquareBrackets<Tail>}`
: Path;

/**
Returns true if `LongString` is made up out of `Substring` repeated 0 or more times.

@example
```
ConsistsOnlyOf<'aaa', 'a'> //=> true
ConsistsOnlyOf<'ababab', 'ab'> //=> true
ConsistsOnlyOf<'aBa', 'a'> //=> false
ConsistsOnlyOf<'', 'a'> //=> true
```
*/
type ConsistsOnlyOf<LongString extends string, Substring extends string> =
LongString extends ''
? true
: LongString extends `${Substring}${infer Tail}`
? ConsistsOnlyOf<Tail, Substring>
: false;

/**
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;
//=> 'foo' | 0
type WithStringsKeys = keyof WithStrings;
//=> 'foo' | '0'
```
*/
type WithStringKeys<BaseType extends Record<string | number, any>> = {
[Key in `${Extract<keyof BaseType, string | number>}`]: BaseType[Key]
};

/**
Get 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.
mmkal marked this conversation as resolved.
Show resolved Hide resolved

Note:
- Returns `unknown` if `Key` is not a property of `BaseType`, since typescript uses structural typing, and it can't be guaranteed that extra properties unknown to the type system will exist at runtime.
- Returns `undefined` from nullish values, to match the behaviour of most deep-key libraries like lodash, dot-prop etc.
*/
type PropertyOf<BaseType, Key extends string> =
BaseType extends null | undefined
? undefined
mmkal marked this conversation as resolved.
Show resolved Hide resolved
: Key extends keyof BaseType
? BaseType[Key]
: BaseType extends {
[n: number]: infer Item;
length: number; // Note - this is needed to avoid being too lax with records types using number keys like { 0: string; 1: boolean }
}
? (
ConsistsOnlyOf<Key, StringDigit> extends true
? Item
: unknown
)
: Key extends keyof WithStringKeys<BaseType>
? WithStringKeys<BaseType>[Key]
: unknown;

// 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.
/**
Get a deeply-nested property from an object using a key path, like Lodash's `.get()` function.

Use-case: Retrieve a property from deep inside an API response or some other complex object.

@example
```
import {Get} from 'type-fest';
import * as lodash from 'lodash';

const get = <BaseType, Path extends string>(object: BaseType, path: Path): Get<BaseType, Path> =>
lodash.get(object, path);

interface ApiResponse {
hits: {
hits: Array<{
_id: string
_source: {
name: Array<{
given: string[]
family: string
}>
birthDate: string
}
}>
}
}

const getName = (apiResponse: ApiResponse) =>
get(apiResponse, 'hits.hits[0]._source.name');
//=> Array<{given: string[]; family: string}>
```
*/
export type Get<BaseType, Path extends string> = GetWithPath<BaseType, ToPath<Path>>;
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];