From 8fa89e37239871fcb1e58e2e49941d066de428ed Mon Sep 17 00:00:00 2001 From: "T. R. Bernstein" <137705289+trbernstein@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:54:48 +0200 Subject: [PATCH] feat(core-extensions): Add Object.mapKeys --- packages/core-extensions/package.json | 10 +++- .../src/internal/mapped-keys.type.ts | 8 +++ .../core-extensions/src/object/map-keys.f.ts | 32 +++++++++++ .../src/object/map-keys.test.ts | 55 +++++++++++++++++++ .../core-extensions/src/object/map-keys.ts | 21 +++++++ 5 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 packages/core-extensions/src/internal/mapped-keys.type.ts create mode 100644 packages/core-extensions/src/object/map-keys.f.ts create mode 100644 packages/core-extensions/src/object/map-keys.test.ts create mode 100644 packages/core-extensions/src/object/map-keys.ts diff --git a/packages/core-extensions/package.json b/packages/core-extensions/package.json index 147a95a..dbbcff5 100644 --- a/packages/core-extensions/package.json +++ b/packages/core-extensions/package.json @@ -29,6 +29,14 @@ "author": "T. R. Bernstein ", "license": "EUPL-1.2", "exports": { + "./object/map-keys.f": { + "import": "./dist/object/map-keys.f.js", + "types": "./dist/object/map-keys.f.d.ts" + }, + "./object/map-keys": { + "import": "./dist/object/map-keys.js", + "types": "./dist/object/map-keys.d.ts" + }, "./object/map-values.f": { "import": "./dist/object/map-values.f.js", "types": "./dist/object/map-values.f.d.ts" @@ -38,4 +46,4 @@ "types": "./dist/object/map-values.d.ts" } } -} +} \ No newline at end of file diff --git a/packages/core-extensions/src/internal/mapped-keys.type.ts b/packages/core-extensions/src/internal/mapped-keys.type.ts new file mode 100644 index 0000000..aca77e4 --- /dev/null +++ b/packages/core-extensions/src/internal/mapped-keys.type.ts @@ -0,0 +1,8 @@ +export type MappedKeys = + T extends Array + ? Array> + : T extends object + ? { + [K in keyof T as R]: MappedKeys + } + : T diff --git a/packages/core-extensions/src/object/map-keys.f.ts b/packages/core-extensions/src/object/map-keys.f.ts new file mode 100644 index 0000000..f359c18 --- /dev/null +++ b/packages/core-extensions/src/object/map-keys.f.ts @@ -0,0 +1,32 @@ +import type { MappedKeys } from '@/internal/mapped-keys.type.js' + +function isRecord(item: any): item is Record { + const isNotNull = item != null + const isObject = typeof item === 'object' + const isNotAnArray = !Array.isArray(item) + return isNotNull && isNotAnArray && isObject +} + +function mapKeysInArrayItems(arr: T, transform: (key: string) => R) { + const result = arr.map((item) => (isRecord(item) ? mapKeys(item, transform) : item)) + + return result +} + +export function mapKeys, R extends string>( + obj: T, + transform: (key: string) => R +): MappedKeys { + const result = >{} + for (let key of Object.keys(obj)) { + let newKey = transform(key) + const value = obj[key] + const newValue = isRecord(value) + ? mapKeys(value, transform) + : Array.isArray(value) + ? mapKeysInArrayItems(value, transform) + : value + result[newKey] = newValue + } + return result +} diff --git a/packages/core-extensions/src/object/map-keys.test.ts b/packages/core-extensions/src/object/map-keys.test.ts new file mode 100644 index 0000000..5503bf7 --- /dev/null +++ b/packages/core-extensions/src/object/map-keys.test.ts @@ -0,0 +1,55 @@ +import { describe, it, test, expect } from 'vitest' +import { mapKeys } from './map-keys.f.js' + +describe('mapKeys', () => { + it('does nothing for empty object', async () => { + const obj = >{} + const result = mapKeys(obj, (key) => `${key}_`) + + expect(result).toEqual(obj) + }) + + it('maps first level keys', async () => { + const obj = { + a: 1, + b: 2 + } + const result = mapKeys(obj, (key) => `${key}_`) + + expect(result).toEqual({ a_: 1, b_: 2 }) + }) + + it('maps second level keys', async () => { + const obj = { + a: { b: 1 } + } + const result = mapKeys(obj, (key) => `${key}_`) + + expect(result).toEqual({ a_: { b_: 1 } }) + }) + + it('maps second level keys in arrays', async () => { + const obj = { + a: [{ b: 1 }] + } + const result = mapKeys(obj, (key) => `${key}_`) + + expect(result).toEqual({ a_: [{ b_: 1 }] }) + }) + + it('maps keys with unusual values', async () => { + const func = () => 1 + const obj = { + a: func, + b: undefined, + c: null + } + const result = mapKeys(obj, (key) => `${key}_`) + + expect(result).toEqual({ + a_: func, + b_: undefined, + c_: null + }) + }) +}) diff --git a/packages/core-extensions/src/object/map-keys.ts b/packages/core-extensions/src/object/map-keys.ts new file mode 100644 index 0000000..8beda87 --- /dev/null +++ b/packages/core-extensions/src/object/map-keys.ts @@ -0,0 +1,21 @@ +import type { MappedKeys } from '@/internal/mapped-keys.type.js' +import { mapKeys } from './map-keys.f.js' + +const objectMapKeys = function ( + this: T, + callback: (key: string) => R +): MappedKeys { + return mapKeys(this, callback) +} + +declare global { + interface Object { + mapKeys: typeof objectMapKeys + } +} + +Object.defineProperty(Object.prototype, 'mapKeys', { + value: objectMapKeys +}) + +export {}