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 KeyPath and PathValue types #158

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ 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.
- [`KeyPath`](ts41/key-path.d.ts) - Represents a dot path to a nested object property.
- [`PathValue`](ts41/key-path.d.ts) - Targets a property from a nested object using a dot path.

### Miscellaneous

Expand Down
21 changes: 21 additions & 0 deletions test-d/key-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {expectError, expectType} from 'tsd';
import {KeyPath, PathValue} from '../ts41';

const user = {
firstName: 'Foo',
lastName: 'Bar',
projects: [
{name: 'Baz', contributors: 68},
{name: 'Waldo', contributors: 12}
]
};

declare function get<T, P extends KeyPath<T>>(object: T, path: P): PathValue<T, P>;

get(user, 'projects.0.name');
expectType<string>(get(user, 'projects.0.name'));
expectType<number>(get(user, 'projects.1.contributors'));

expectError(get(user, 'projects.2.name'));
expectError(get(user, 'projects.name'));
expectError(get(user, 'name'));
1 change: 1 addition & 0 deletions ts41/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {KebabCase} from './kebab-case';
export {PascalCase} from './pascal-case';
export {SnakeCase} from './snake-case';
export {DelimiterCase} from './delimiter-case';
export {KeyPath, PathValue} from './key-path';
64 changes: 64 additions & 0 deletions ts41/key-path.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Targets a property from a nested object using a dot path.
*
* Use case: Extracting a given value from a complex API requests that is otherwise non-essential outside that value.
*
* @see KeyPath
* @example
* declare function get<T, P extends KeyPath<T>>(obj: T, path: P): PathValue<T, P>;
* const user = {
* firstName: 'Foo',
* lastName: 'Bar',
* projects: [
* { name: 'Baz', contributors: 68 },
* { name: 'Waldo', contributors: 12 },
* ]
* };
*
* get(user, 'projects.0.name');
*/
export type PathValue<ObjectType, PathType extends KeyPath<ObjectType>> =
PathType extends `${infer KeyType}.${infer Rest}`
? KeyType extends keyof ObjectType
? Rest extends KeyPath<ObjectType[KeyType]>
? PathValue<ObjectType[KeyType], Rest>
: never
: never
: PathType extends keyof ObjectType
? ObjectType[PathType]
: never;

/**
* Represents a dot path to a nested object property.
*
* Use case: to be used in any helper function that extracts values based on a string dot path, like [lodash.get](https://lodash.com/docs/#get) or [dot-prop](https://github.com/sindresorhus/dot-prop).
*
* @see PathValue
* @example
* declare function get<T, P extends KeyPath<T>>(obj: T, path: P): PathValue<T, P>;
* const user = {
* firstName: 'Foo',
* lastName: 'Bar',
* projects: [
* { name: 'Baz', contributors: 68 },
* { name: 'Waldo', contributors: 12 },
* ]
* };
*
* get(user, 'projects.0.name');
*/
export type KeyPath<ObjectType> =
ImpliedPath<ObjectType> extends string | keyof ObjectType
? ImpliedPath<ObjectType>
: keyof ObjectType;

type ImpliedPath<ObjectType> = ImpliedPathDeep<ObjectType, keyof ObjectType> | keyof ObjectType;

type ImpliedPathDeep<ObjectType, KeyType extends keyof ObjectType> =
KeyType extends string
? ObjectType[KeyType] extends Record<string, any>
?
| `${KeyType}.${ImpliedPathDeep<ObjectType[KeyType], Exclude<keyof ObjectType[KeyType], keyof any[]>> & string}`
| `${KeyType}.${Exclude<keyof ObjectType[KeyType], keyof any[]> & string}`
: never
: never;