From 53086025d02c2f08a18f694be7db35bbecefc125 Mon Sep 17 00:00:00 2001 From: Robin Pokorny Date: Fri, 16 Jun 2023 10:41:54 +0200 Subject: [PATCH] Support symbol properties (#10) * support symbol properties * fix type to support all peoperty keys --- src/index.test.ts | 27 +++++++++++++++++++++++++++ src/index.ts | 15 ++++++++++++--- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index b69f2ef..835e2f9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -18,6 +18,7 @@ type User = { emails: string[]; toString: string; constructor: string | null; + [Symbol.isConcatSpreadable]: boolean; }; describe("defaultComposer", () => { @@ -38,6 +39,7 @@ describe("defaultComposer", () => { emails: ["contact@aralroca.com"], hobbies: ["programming"], toString: "I am Aral", + [Symbol.isConcatSpreadable]: false, }; const originalObject = { @@ -70,6 +72,7 @@ describe("defaultComposer", () => { hobbies: ["parkour", "computer science", "books", "nature"], toString: "I am Aral", constructor: null, + [Symbol.isConcatSpreadable]: false, }; expect(defaultComposer(defaults, originalObject)).toEqual(expected); @@ -137,6 +140,30 @@ describe("defaultComposer", () => { expect(mockFn).toBeCalledTimes(1); }); + it("should work with enumerable symbol properties", () => { + const tag = Symbol.for("tag"); + const version = Symbol.for("version"); + const createdAt = Symbol.for("createdAt"); + + const defaults = { + [tag]: "user", + [createdAt]: Date.UTC(2020, 1, 1), + }; + + Object.defineProperty(defaults, version, { value: 1, enumerable: false }); + + const object = { + [createdAt]: Date.UTC(2023, 6, 1), + }; + + const expected = { + [tag]: "user", + [createdAt]: Date.UTC(2023, 6, 1), + }; + + expect(defaultComposer(defaults, object)).toEqual(expected); + }); + it("should work with a custom isDefaultableValue", () => { const defaults = { original: { diff --git a/src/index.ts b/src/index.ts index cf38a21..b5b3d30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ type isDefaultableValueInputType = { defaultableValue: boolean; - key: string; + key: PropertyKey; value: unknown; }; @@ -26,7 +26,7 @@ export function defaultComposer(...args: Partial[]): T { function compose(defaults: Partial, obj: Partial): Partial { const result: Partial = {}; - const allKeys = new Set([defaults, obj].flatMap(Object.keys)); + const allKeys = new Set([defaults, obj].flatMap(getAllKeys)); for (let key of allKeys) { const defaultsValue = defaults[key]; @@ -60,7 +60,7 @@ function isObject(value: any): boolean { function isEmptyObjectOrArray(object: T): boolean { if (typeof object !== "object" || object === null) return false; - return Object.keys(object).length === 0; + return getAllKeys(object).length === 0; } function checkDefaultableValue({ value }: { value: unknown }): boolean { @@ -78,3 +78,12 @@ function hasOwn( ): key is T { return Object.prototype.hasOwnProperty.call(obj, key); } + +function getAllKeys(object: {}): PropertyKey[] { + return [ + ...Object.keys(object), + ...Object.getOwnPropertySymbols(object).filter( + (key) => Object.getOwnPropertyDescriptor(object, key)?.enumerable + ), + ]; +}