feat(core-extensions): Add Object.mapKeys

This commit is contained in:
T. R. Bernstein
2025-07-03 22:54:48 +02:00
parent 6f13c15c4c
commit 8fa89e3723
5 changed files with 125 additions and 1 deletions

View File

@@ -29,6 +29,14 @@
"author": "T. R. Bernstein <ljspkgs01-project@tabshift.dev>",
"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"
}
}
}
}

View File

@@ -0,0 +1,8 @@
export type MappedKeys<T, R extends string> =
T extends Array<infer U>
? Array<MappedKeys<U, R>>
: T extends object
? {
[K in keyof T as R]: MappedKeys<T[K], R>
}
: T

View File

@@ -0,0 +1,32 @@
import type { MappedKeys } from '@/internal/mapped-keys.type.js'
function isRecord(item: any): item is Record<string, any> {
const isNotNull = item != null
const isObject = typeof item === 'object'
const isNotAnArray = !Array.isArray(item)
return isNotNull && isNotAnArray && isObject
}
function mapKeysInArrayItems<T extends any[], R extends string>(arr: T, transform: (key: string) => R) {
const result = arr.map((item) => (isRecord(item) ? mapKeys(item, transform) : item))
return result
}
export function mapKeys<T extends Record<string, any>, R extends string>(
obj: T,
transform: (key: string) => R
): MappedKeys<T, R> {
const result = <MappedKeys<T, R>>{}
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
}

View File

@@ -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 = <Record<string, any>>{}
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
})
})
})

View File

@@ -0,0 +1,21 @@
import type { MappedKeys } from '@/internal/mapped-keys.type.js'
import { mapKeys } from './map-keys.f.js'
const objectMapKeys = function <T extends object, R extends string>(
this: T,
callback: (key: string) => R
): MappedKeys<T, R> {
return mapKeys(this, callback)
}
declare global {
interface Object {
mapKeys: typeof objectMapKeys
}
}
Object.defineProperty(Object.prototype, 'mapKeys', {
value: objectMapKeys
})
export {}