Skip to content

Commit

Permalink
feat(micro-dash): add property()
Browse files Browse the repository at this point in the history
  • Loading branch information
ersimont committed Oct 3, 2021
1 parent ad11531 commit 52a3c02
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 166 deletions.
3 changes: 3 additions & 0 deletions projects/micro-dash-sizes/src/app/util/property.lodash.ts
@@ -0,0 +1,3 @@
import property from 'lodash-es/property';

console.log(property(['a', 'b'])({ a: 1 }));
3 changes: 3 additions & 0 deletions projects/micro-dash-sizes/src/app/util/property.microdash.ts
@@ -0,0 +1,3 @@
import { property } from '@s-libs/micro-dash';

console.log(property(['a', 'b'])({ a: 1 }));
16 changes: 8 additions & 8 deletions projects/micro-dash/src/lib/object/get.ts
@@ -1,5 +1,5 @@
import { IfCouldBe, Key, Nil } from '../interfaces';
import { castArray } from '../lang';
import { castArray, isUndefined } from '../lang';

type WithDefault<V, D> =
| (undefined extends D ? V : Exclude<V, undefined>)
Expand All @@ -13,7 +13,7 @@ type WithDefault<V, D> =
*
* Contribution to minified bundle size, when it is the only function imported:
* - Lodash: 5,123 bytes
* - Micro-dash: 110 bytes
* - Micro-dash: 107 bytes
*/

export function get<T, K extends keyof NonNullable<T>, D = undefined>(
Expand Down Expand Up @@ -77,15 +77,15 @@ export function get(
path: Key | readonly Key[],
defaultValue?: any,
): any {
// const val = property(path)(object);
// return isUndefined(val) ? defaultValue : val;
path = castArray(path);
const val = getWithoutDefault(castArray(path), object);
return isUndefined(val) ? defaultValue : val;
}

export function getWithoutDefault(path: readonly any[], object: any): any {
const length = path.length;
let index = 0;
while (object != null && index < length) {
object = object[path[index++]];
}
return !index || index < length || object === undefined
? defaultValue
: object;
return !index || index < length ? undefined : object;
}
1 change: 1 addition & 0 deletions projects/micro-dash/src/lib/util/index.ts
Expand Up @@ -4,6 +4,7 @@ export { flowRight } from './flow-right';
export { identity } from './identity';
export { matches } from './matches';
export { noop } from './noop';
export { property } from './property';
export { range } from './range';
export { times } from './times';
export { uniqueId } from './unique-id';
188 changes: 70 additions & 118 deletions projects/micro-dash/src/lib/util/property.spec.ts
@@ -1,119 +1,71 @@
// There's no good way to make this type safe.
import { expectTypeOf } from 'expect-type';
import { property } from './property';

// import { property } from "./property";
//
// describe("property()", () => {
// //
// // stolen from https://github.com/lodash/lodash
// //
//
// it("should create a function that plucks a property value of a given object", () => {
// const object = { a: 1 };
// const prop = property<typeof object>(["a"]);
// expect(prop.length).toBe(1);
// expect(prop(object)).toBe(1);
// });
//
// it("should pluck deep property values", () => {
// expect(property(["a", "b"])({ a: { b: 2 } })).toBe(2);
// });
//
// it("should work with a non-string `path`", () => {
// expect(property([1])([1, 2, 3])).toBe(2);
// });
//
// // it("should preserve the sign of `0`", () => {
// // const object = { "-0": "a", "0": "b" },
// // props = [-0, Object(-0), 0, Object(0)];
// //
// // const actual = lodashStable.map(props, function(key) {
// // const prop = _.property(key);
// // return prop(object);
// // });
// //
// // assert.deepEqual(actual, ["a", "a", "b", "b"]);
// // });
// //
// // it("should coerce `path` to a string", () => {
// // function fn() {}
// // fn.toString = lodashStable.constant("fn");
// //
// // const expected = [1, 2, 3, 4],
// // object = { null: 1, undefined: 2, fn: 3, "[object Object]": 4 },
// // paths = [null, undefined, fn, {}];
// //
// // lodashStable.times(2, function(index) {
// // const actual = lodashStable.map(paths, function(path) {
// // const prop = _.property(index ? [path] : path);
// // return prop(object);
// // });
// //
// // assert.deepEqual(actual, expected);
// // });
// // });
// //
// // it("should pluck a key over a path", () => {
// // const object = { "a.b": 1, a: { b: 2 } };
// //
// // lodashStable.each(["a.b", ["a.b"]], function(path) {
// // const prop = _.property(path);
// // assert.strictEqual(prop(object), 1);
// // });
// // });
// //
// // it("should return `undefined` when `object` is nullish", function(
// // assert,
// // ) {
// // const values = [, null, undefined],
// // expected = lodashStable.map(values, noop);
// //
// // lodashStable.each(["constructor", ["constructor"]], function(path) {
// // const prop = _.property(path);
// //
// // const actual = lodashStable.map(values, function(value, index) {
// // return index ? prop(value) : prop();
// // });
// //
// // assert.deepEqual(actual, expected);
// // });
// // });
// //
// // it(
// // "should return `undefined` for deep paths when `object` is nullish",
// // () => {
// // const values = [, null, undefined],
// // expected = lodashStable.map(values, noop);
// //
// // lodashStable.each(
// // [
// // "constructor.prototype.valueOf",
// // ["constructor", "prototype", "valueOf"],
// // ],
// // function(path) {
// // const prop = _.property(path);
// //
// // const actual = lodashStable.map(values, function(value, index) {
// // return index ? prop(value) : prop();
// // });
// //
// // assert.deepEqual(actual, expected);
// // },
// // );
// // },
// // );
// //
// // it(
// // "should return `undefined` if parts of `path` are missing",
// // () => {
// // const object = {};
// //
// // lodashStable.each(
// // ["a", "a[1].b.c", ["a"], ["a", "1", "b", "c"]],
// // function(path) {
// // const prop = _.property(path);
// // assert.strictEqual(prop(object), undefined);
// // },
// // );
// // },
// // );
// });
describe('property()', () => {
it('has fancy typing', () => {
expect().nothing();

interface O {
a: number;
b: Array<{ c: 3 }>;
d?: { e: Date };
}

const obj: O = {
a: 1,
b: [{ c: 3 }],
};
expectTypeOf(property(['a'])(obj)).toEqualTypeOf<number>();
expectTypeOf(property(['b'])(obj)).toEqualTypeOf<Array<{ c: 3 }>>();
expectTypeOf(property(['b', 0])(obj)).toEqualTypeOf<{ c: 3 }>();
expectTypeOf(property(['b', 0, 'c'])(obj)).toEqualTypeOf<3>();

expectTypeOf(property(['d'])(obj)).toEqualTypeOf<{ e: Date } | undefined>();
const blah = property(['d', 'e'])(obj);
expectTypeOf(blah).toEqualTypeOf<Date | undefined>();

const oOrN = obj as O | null;
const blah2 = property(['a'])(oOrN);
expectTypeOf(blah2).toEqualTypeOf<number | undefined>();

expectTypeOf(property(['a'])(undefined)).toEqualTypeOf<undefined>();
});

//
// stolen from https://github.com/lodash/lodash
//

it('should create a function that plucks a property value of a given object', () => {
const object = { a: 1 };
const prop = property(['a']);
expect(prop.length).toBe(1);
expect(prop(object)).toBe(1);
});

it('should pluck deep property values', () => {
expect(property(['a', 'b'])({ a: { b: 2 } })).toBe(2);
});

it('should work with a non-string `path`', () => {
expect(property([1])([1, 2, 3])).toBe(2);
});

it('should return `undefined` when `object` is nullish', () => {
expect(property(['constructor'])(null)).toBe(undefined);
expect(property(['constructor'])(undefined)).toBe(undefined);
});

it('should return `undefined` for deep paths when `object` is nullish', () => {
expect(property(['constructor', 'prototype', 'valueOf'])(null)).toBe(
undefined,
);
expect(property(['constructor', 'prototype', 'valueOf'])(undefined)).toBe(
undefined,
);
});

it('should return `undefined` if parts of `path` are missing', () => {
expect(property(['a'])({})).toBe(undefined);
expect(property(['a', '1', 'b', 'c'])({})).toBe(undefined);
});
});
68 changes: 28 additions & 40 deletions projects/micro-dash/src/lib/util/property.ts
@@ -1,41 +1,29 @@
// There's no good way to make this type safe.
import { IfCouldBe, Nil } from '../interfaces';
import { getWithoutDefault } from '../object/get';

// import { Function1 } from "ng-dev";
//
// /**
// * Creates a function that returns the value at `path` of a given object.
// *
// * Contribution to minified bundle size, when it is the only function imported:
// */
//
// export function property<T, K1 extends keyof T>(
// path: [K1],
// ): Function1<T, T[K1]>;
// export function property<T, K1 extends keyof T, K2 extends keyof T[K1]>(
// path: [K1, K2],
// ): Function1<T, T[K1][K2]>;
// export function property<
// T,
// K1 extends keyof T,
// K2 extends keyof T[K1],
// K3 extends keyof T[K1][K2]
// >(path: [K1, K2, K3]): Function1<T, T[K1][K2][K3]>;
// export function property<
// T,
// K1 extends keyof T,
// K2 extends keyof T[K1],
// K3 extends keyof T[K1][K2],
// K4 extends keyof T[K1][K2][K3]
// >(path: [K1, K2, K3, K4]): Function1<T, T[K1][K2][K3][K4]>;
// // export function property(path: string[]): any;
//
// export function property(path: string[]) {
// const length = path.length;
// return (object: any) => {
// let index = 0;
// while (object != null && index < length) {
// object = object[path[index++]];
// }
// return !index || index < length ? undefined : object;
// };
// }
// https://stackoverflow.com/a/64776616/1836506
type First<T extends any[]> = T extends [infer U, ...any[]] ? U : never;
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;
type PropertyAtPath<T, Path extends any[]> = First<Path> extends never
? T
: First<Path> extends keyof NonNullable<T>
?
| PropertyAtPath<NonNullable<T>[First<Path>], Rest<Path>>
| IfCouldBe<T, Nil, undefined>
: undefined;

/**
* Creates a function that returns the value at `path` of a given object.
*
* Differences from lodash:
* - does not handle a dot-separated string for `path`
*
* Contribution to minified bundle size, when it is the only function imported:
* - Lodash: 5,261 bytes
* - Micro-dash: 127 bytes
*/
export function property<P extends PropertyKey[]>(
path: readonly [...P],
): <T>(object: T) => PropertyAtPath<T, P> {
return getWithoutDefault.bind(0, path);
}

0 comments on commit 52a3c02

Please sign in to comment.