Stacks Object Utilities
Type-safe object manipulation helpers. All functions are exported from a single file.
Key Path
- Core package:
storage/framework/core/objects/src/index.ts(single file, all functions)
Dependencies
@stacksjs/types--DeepMergetype@stacksjs/utils--notNullishfilter function@stacksjs/validation--isObjecttype guard
Functions
objectMap -- Transform Object Key/Value Pairs
function objectMap<K extends string, V, NK = K, NV = V>(
obj: Record<K, V>,
fn: (key: K, value: V) => [NK, NV] | undefined,
): Record<K, V>
The callback receives (key, value) and must return either:
[newKey, newValue]tuple to include the entryundefinedto filter out the entry
Implementation uses Object.fromEntries(Object.entries(obj).map(...).filter(notNullish)).
// Transform values
objectMap({ a: 1, b: 2 }, (k, v) => [k, v * 2])
// { a: 2, b: 4 }
// Rename keys
objectMap({ a: 1, b: 2 }, (k, v) => [`prefix_${k}`, v])
// { prefix_a: 1, prefix_b: 2 }
// Swap key/value
objectMap({ a: 1, b: 2 }, (k, v) => [v, k])
// { 1: 'a', 2: 'b' }
// Filter entries (return undefined to exclude)
objectMap({ a: 1, b: 2, c: 3 }, (k, v) => k === 'b' ? undefined : [k, v])
// { a: 1, c: 3 }
deepMerge -- Deep Merge Objects
function deepMerge<T extends object = object, S extends object = T>(
target: T,
...sources: S[]
): DeepMerge<T, S>
Recursively merges source objects into target. Handles multiple sources via recursive spread.
Key behaviors:
- Only merges "mergeable objects" (objects that are not arrays, checked via
isObject()and!Array.isArray()) - Arrays are NOT deeply merged -- they are replaced entirely
- Missing keys in target are created as empty objects
{}before recursive merge - Mutates the target object -- the return value IS the modified target
- Processes sources left to right
const target = { a: 1, nested: { x: 1, y: 2 } }
const source = { b: 2, nested: { y: 3, z: 4 } }
const result = deepMerge(target, source)
// result === target (same reference, mutated)
// { a: 1, b: 2, nested: { x: 1, y: 3, z: 4 } }
// Multiple sources
const merged = deepMerge(defaults, userConfig, overrides)
The DeepMerge<T, S> type (from @stacksjs/types) provides full type inference:
type DeepMerge<F, S> = MergeInsertions<{
[K in keyof F | keyof S]: K extends keyof S & keyof F
? DeepMerge<F[K], S[K]>
: K extends keyof S ? S[K]
: K extends keyof F ? F[K]
: never
}>
objectPick -- Pick Properties by Key
function objectPick<O extends object, T extends keyof O>(
obj: O,
keys: T[],
omitUndefined?: boolean, // default: false
): Pick<O, T>
Creates a new object with only the specified keys. Uses reduce internally.
const user = { id: 1, name: 'John', email: 'john@test.com', password: 'secret' }
objectPick(user, ['id', 'name', 'email'])
// { id: 1, name: 'John', email: 'john@test.com' }
// With omitUndefined=true, skips keys whose values are undefined
const partial = { id: 1, name: 'John', bio: undefined }
objectPick(partial, ['id', 'name', 'bio'], true)
// { id: 1, name: 'John' } (bio omitted)
- Only includes keys that exist in the object (
k in objcheck) - With
omitUndefined=true, additionally skips keys whereobj[k] === undefined
clearUndefined -- Remove Undefined Fields
function clearUndefined<T extends object>(obj: T): T
Mutates the object by deleting keys whose values are undefined. Returns the same object reference.
const data = { name: 'John', age: undefined, email: 'john@test.com' }
clearUndefined(data)
// data is now { name: 'John', email: 'john@test.com' }
// Returns same reference as input
- Only removes
undefinedvalues --null,0,'',falseare preserved - Uses
delete obj[key]for removal
isKeyOf -- Type Guard for Object Keys
function isKeyOf<T extends object>(obj: T, k: keyof any): k is keyof T
Type guard that narrows k to keyof T if the key exists in obj. Uses k in obj.
const config = { debug: true, verbose: false }
const key: string = 'debug'
if (isKeyOf(config, key)) {
// TypeScript knows: key is 'debug' | 'verbose'
console.log(config[key]) // type-safe access
}
objectKeys -- Strictly Typed Object.keys
function objectKeys<T extends object>(obj: T): Array<`${keyof T & (string | number | boolean | null | undefined)}`>
Returns Object.keys(obj) with a stricter return type than the built-in Object.keys() (which returns string[]).
const user = { name: 'John', age: 30 }
const keys = objectKeys(user)
// Type: ('name' | 'age')[] (not string[])
objectEntries -- Strictly Typed Object.entries
function objectEntries<T extends object>(obj: T): Array<[keyof T, T[keyof T]]>
Returns Object.entries(obj) with a stricter return type than the built-in Object.entries() (which returns [string, any][]).
const user = { name: 'John', age: 30 }
const entries = objectEntries(user)
// Type: ['name' | 'age', string | number][]
hasOwnProperty -- Safe Property Check
function hasOwnProperty<T>(obj: T, v: PropertyKey): boolean
Null-safe wrapper around Object.prototype.hasOwnProperty.call(). Returns false if obj is null or undefined.
hasOwnProperty({ a: 1 }, 'a') // true
hasOwnProperty({ a: 1 }, 'b') // false
hasOwnProperty(null, 'a') // false (no throw)
hasOwnProperty(undefined, 'a') // false (no throw)
Gotchas
deepMergemutates the target object -- it does not create a new object. The return value is the same reference astarget.clearUndefinedmutates the input object -- it deletes properties in place.objectPickcreates a NEW object -- original is not affected.objectMapcreates a NEW object viaObject.fromEntries.deepMergedoes NOT deeply merge arrays -- arrays from source replace arrays in target entirely.deepMergeusesisObject()from@stacksjs/validationcombined with!Array.isArray()to determine mergeability.objectMapcan filter entries by returningundefinedfrom the callback -- filtered vianotNullishfrom@stacksjs/utils.objectKeysreturn type uses a template literal type to handle non-string keys.clearUndefinedonly removesundefined--nullvalues are preserved.hasOwnPropertyacceptsnull/undefinedobjects without throwing.objectPickwithomitUndefined=false(default) includes keys even if their values areundefined, as long as the key exists in the object.- These utilities are also re-exported through
@stacksjs/utils.