Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"recursive-readdir-sync": "^1.0.6",
"ts-expect": "^1.1.0",
"ts-jest": "^24.1.0",
"typescript": "^3.6.3",
"typescript": "^4.1.6",
"walker": "^1.0.7"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/is/__tests__/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('utils/is/equal', () => {
expect(isEqual(new Date(), new Date(123))).toBe(false);
expect(isEqual(/123/, /123/)).toBe(true);
expect(isEqual(/123/, /1234/)).toBe(false);
expect(isEqual(() => 3, () => 3)).toBe(true);
expect(isEqual(() => 3, () => 3)).toBe(false);
expect(isEqual(() => 3, () => 4)).toBe(false);
});

Expand Down
2 changes: 1 addition & 1 deletion src/is/__tests__/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('utils/is/promise', () => {
expect(isPromise(() => {})).toBe(false);
expect(isPromise(Promise.resolve())).toBe(true);
expect(isPromise(Promise.reject())).toBe(true);
expect(isPromise(new Promise((res) => res()))).toBe(true);
expect(isPromise(new Promise<void>((res) => res()))).toBe(true);
const f = () => {};

expect(isPromise(f)).toBe(false);
Expand Down
99 changes: 99 additions & 0 deletions src/object/__tests__/path.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { expectType } from 'ts-expect';
import path from '../path';

describe('utils/object/path', () => {
Expand All @@ -6,4 +7,102 @@ describe('utils/object/path', () => {
expect(path(['a', 'b'], { a: { c: 3 } })).toBeUndefined();
expect(path(['test'])({ test: 'test' })).toBe('test');
});

it('should perform dirty cast when type parameter is provided', () => {
const obj = { firstName: 'john' } as const;
const result1 = path<number>(['firstName', 'some-non-existent-prop'], obj);
const result2 = path<number>(['firstName', 'some-non-existent-prop'])(obj);

expectType<number | undefined>(result1);
expectType<number | undefined>(result2);
});

it('should infer type as `unknown` by default', () => {
const obj = { firstName: 'john' } as const;
const result1 = path(['firstName'], obj);
const result2 = path(['firstName'])(obj);

expectType<unknown>(result1);
expectType<unknown>(result2);
});

it('should infer type with a tuple of single value', () => {
const obj = { firstName: 'john' } as const;
const result1 = path(['firstName'] as const, obj);
const result2 = path(['firstName'] as const)(obj);

expectType<'john'>(result1);
expectType<'john'>(result2);
});

it('should infer type with a tuple of multiple values', () => {
const obj = { user: { firstName: 'john' } } as const;
const result1 = path(['user', 'firstName'] as const, obj);
const result2 = path(['user', 'firstName'] as const)(obj);

expectType<'john'>(result1);
expectType<'john'>(result2);
});

it('should infer type with a tuple containing arbitrary id for a record', () => {
const users: Record<string, { name: string }> = {
'some-id': { name: 'john' }
}
const obj = { users } as const;
const result1 = path(['users', 'some-id', 'name'] as const, obj);
const result2 = path(['users', 'some-other-id', 'name'] as const)(obj);

expectType<string | undefined>(result1);
expectType<string | undefined>(result2);
});

it('should infer type with a tuple containing arbitrary index for an array', () => {
const users = [{ name: 'john' }]
const obj = { users } as const;
const result1 = path(['users', 0, 'name'] as const, obj);
const result2 = path(['users', 1, 'name'] as const)(obj);

expectType<string | undefined>(result1);
expectType<string | undefined>(result2);
});

it('should infer type with a tuple containing a field with an optional value', () => {
type User = {
fio?: {
firstName: string;
lastName: string;
},
}
const obj: User = {
fio: {
firstName: 'john',
lastName: 'doe'
}
};
const result1 = path(['fio', 'lastName'] as const, obj);
const result2 = path(['fio', 'lastName'] as const)(obj);

expectType<string | undefined>(result1);
expectType<string | undefined>(result2);
});

it('should infer type with a tuple containing a field with a nullable value', () => {
type User = {
fio: {
firstName: string;
lastName: string;
} | null,
}
const obj: User = {
fio: {
firstName: 'john',
lastName: 'doe'
}
};
const result1 = path(['fio', 'lastName'] as const, obj);
const result2 = path(['fio', 'lastName'] as const)(obj);

expectType<string | undefined>(result1);
expectType<string | undefined>(result2);
});
});
172 changes: 141 additions & 31 deletions src/object/path.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,145 @@
import { Prop, Paths } from '../typings/types';
import type { Prop, Paths } from "../typings/types";

type If<B, F, S> = B extends true ? F : S;
type Or<B1, B2> = B1 extends true ? true : B2;
type Not<B> = B extends true ? false : true;
type Has<U extends any, U1 extends any> = [U1] extends [U] ? true : false;

type IsExactKey<T> = string extends T
? false
: number extends T
? false
: symbol extends T
? false
: true;

type ValueByPath<P, O, U = false> = P extends readonly [infer F, ...(infer R)]
? /**
* In case we accessed optional property of O on the previous step
* O can be `undefined`. keyof operator won't work on a union
*/
Or<Has<O, null>, Has<O, undefined>> extends infer HasNullOrUndefined
? Exclude<O, undefined | null> extends infer O2
? O2 extends object
? F extends keyof O2
? R extends []
? If<Or<U, HasNullOrUndefined>, O2[F] | undefined, O2[F]>
: ValueByPath<
R,
O2[F],
Or<
Or<U, HasNullOrUndefined>,
/**
* In case we run into some kind of dynamic dictionary
* something like Record<string, ..> or Record<number, ..>
* We want to make sure that we get T | undefined instead of T as a result
*/
Not<IsExactKey<keyof O2>>
>
>
: undefined
: never
: never
: never
: never;

export interface Path {
<K extends Prop, O extends Record<K, any>>(path: [K], obj: O): O[K];
<K extends Prop>(path: [K]): <O extends Record<K, any>>(obj: O) => O[K];
<T>(path: Paths, obj): T | undefined;
<T>(path: Paths): (obj) => T | undefined;
}

import curryN from '../function/curryN';

/**
* Retrieve the value at a given path.
*
* @param {[String]} paths The path to use.
* @param {Object} obj The object to retrieve the nested property from.
* @return {*} The data at `path`.
* @example
*
* path(['a', 'b'], {a: {b: 2}}); //=> 2
* path(['a', 'b'], {c: {b: 2}}); //=> undefined
*/
export default curryN(2, <K extends Prop, O extends Record<K, any>>(paths: Paths = [], obj: O = {} as any) => {
let val = obj;

for (let i = 0; i < paths.length; i++) {
if (val == null) {
return;
}

val = val[paths[i]];
/**
* Retrieve the value at a given path.
* **Note:** Use `as const` cast on the `paths` for type inference.
*
* @param {[String]} paths The path to use.
* @param {Object} obj The object to retrieve the nested property from.
* @return {*} The data at `path`.
* @example
*
* path(['a', 'b'], {a: {b: 2}}); //=> 2
* path(['a', 'b'], {c: {b: 2}}); //=> undefined
*/
(pathToProp: Prop[], obj: object): unknown;
/**
* Retrieve the value at a given path.
* **Note:** Use `as const` cast on the `paths` for type inference.
*
* @param {[String]} paths The path to use.
* @return {*} function to get data at `path` for a given object
* @example
*
* path(['a', 'b'])({a: {b: 2}}); //=> 2
* path(['a', 'b'])({c: {b: 2}}); //=> undefined
*/
(pathToProp: Prop[]): (obj: object) => unknown;
/**
* @deprecated
* Please use `path` without type parameters instead.
* Make sure to use `as const` cast for props array for type inference
* @example
*
* path(['a', 'b'] as const, { a: { b: 2 } });
*/
<T>(pathToProp: Prop[], obj: object): T | undefined;
/**
* @deprecated
* Please use `path` without type parameters instead.
* Make sure to use `as const` cast for props array for type inference
* @example
*
* path(['a', 'b'] as const)({ a: { b: 2 } });
*/
<T>(pathToProp: Prop[]): (obj: object) => T | undefined;
/**
* Retrieve the value at a given path.
*
* @param {[String]} paths The path to use.
* @param {Object} obj The object to retrieve the nested property from.
* @return {*} The data at `path`.
* @example
*
* const johnDoe = {
* fio: {
* firstName: 'John',
* lastName: 'Doe',
* },
* };
* const firstName = path(['fio', 'firstName'] as const, johnDoe); // => 'John'
*/
<P extends Paths, O>(pathToProp: P, obj: O): ValueByPath<P, O>;
/**
* Retrieve the value at a given path.
*
* @param {[String]} paths The path to use.
* @return {*} function to get data at `path` for a given object
* @example
*
* const johnDoe = {
* fio: {
* firstName: 'John',
* lastName: 'Doe',
* },
* };
* const getLastName = path(['fio', 'lastName'] as const);
* const lastName = getLastName(johnDoe); // => 'Doe'
*/
<P extends Paths>(pathToProp: P): <O>(obj: O) => ValueByPath<P, O>;
};

import curryN from "../function/curryN";

const _path = <K extends Prop, O extends Record<K, any>>(
paths: Paths = [],
obj: O = {} as any
) => {
let val = obj;

for (let i = 0; i < paths.length; i++) {
if (val == null) {
return undefined;
}

return val;
}) as Path;
val = val[paths[i]];
}

return val;
};

export default curryN(2, _path) as Path;