Object
31 exports from @ontologyio/receta/object.
Everything exported from @ontologyio/receta/object. New here? Start with the Object guide.
Functions
compact
Removes all nullish (null or undefined) values from an object (shallow).
Creates a new object without properties that have null or undefined values. Useful for cleaning data, preparing payloads, and removing empty fields.
Note: This is a shallow operation. Nested objects are not compacted.
function compact<T extends PlainObject>(obj: T): Partial<T>const input = {
name: 'Alice',
age: null,
email: 'alice@example.com',
phone: undefined
}
compact(input)
// => { name: 'Alice', email: 'alice@example.com' }
// Cleaning API response
pipe(
apiResponse,
compact,
validateShape(userSchema)
)
// Note: Keeps falsy values like 0, false, ''
compact({ count: 0, active: false, name: '' })
// => { count: 0, active: false, name: '' }See also stripUndefined.
deepMerge
Deep merges multiple objects with configurable conflict resolution.
Recursively merges objects, with later objects taking precedence over earlier ones. Arrays and nested objects can be merged according to specified strategies.
function deepMerge(objects: readonly PlainObject[], options?: DeepMergeOptions | undefined): PlainObjectfunction deepMerge(options?: DeepMergeOptions | undefined): (objects: readonly PlainObject[]) => PlainObject// Basic deep merge
const defaults = { theme: 'light', features: { search: true } }
const config = { features: { export: true } }
deepMerge([defaults, config])
// => { theme: 'light', features: { search: true, export: true } }
// Array replace strategy (default)
deepMerge([
{ tags: ['a', 'b'] },
{ tags: ['c'] }
])
// => { tags: ['c'] }
// Array concat strategy
deepMerge([
{ tags: ['a', 'b'] },
{ tags: ['c'] }
], { arrayStrategy: 'concat' })
// => { tags: ['a', 'b', 'c'] }
// Custom merge function
deepMerge([obj1, obj2], {
customMerge: (key, target, source) => {
if (key === 'count') return (target as number) + (source as number)
return source
}
})
// Data-last (in pipe)
pipe(
defaults,
(defaults) => deepMerge([defaults, userConfig])
)See also Remeda.merge, Remeda.mergeDeep.
filterKeys
Filters an object by keeping only keys that match a predicate.
Creates a new object containing only entries whose keys satisfy the predicate. Useful for filtering by key patterns, removing prefixed keys, or selective property inclusion based on key names.
function filterKeys<T extends PlainObject>(obj: T, predicate: (key: string) => boolean): Partial<T>function filterKeys(predicate: (key: string) => boolean): <T extends PlainObject>(obj: T) => Partial<T>// Data-first
const config = {
api_key: 'secret',
api_url: 'https://api.example.com',
db_host: 'localhost',
db_port: 5432
}
filterKeys(config, (key) => key.startsWith('api_'))
// => { api_key: 'secret', api_url: 'https://api.example.com' }
// Remove private keys
filterKeys(obj, (key) => !key.startsWith('_'))
// Keep only specific patterns
filterKeys(data, (key) => /^[a-z]+$/.test(key))
// Data-last (in pipe)
pipe(
config,
filterKeys((key) => !key.includes('secret'))
)See also filterValues, mask.
filterValues
Filters an object by keeping only entries whose values match a predicate.
Creates a new object containing only entries whose values satisfy the predicate. Useful for removing empty values, filtering by type, or keeping only specific value ranges.
function filterValues<T extends PlainObject>(obj: T, predicate: (value: T[keyof T], key: keyof T) => boolean): Partial<T>function filterValues(predicate: (value: any, key: string) => boolean): <T extends PlainObject>(obj: T) => Partial<T>// Data-first
const scores = { alice: 85, bob: 60, charlie: 92, diana: 55 }
filterValues(scores, (score) => score >= 70)
// => { alice: 85, charlie: 92 }
// Remove empty strings
filterValues({ a: 'hello', b: '', c: 'world' }, (v) => v !== '')
// => { a: 'hello', c: 'world' }
// Type filtering
const mixed = { a: 1, b: 'two', c: 3, d: 'four' }
filterValues(mixed, (v) => typeof v === 'number')
// => { a: 1, c: 3 }
// With key and value
filterValues(obj, (value, key) => value > threshold[key])
// Data-last (in pipe)
pipe(
data,
filterValues((v) => v != null)
)See also filterKeys, compact.
flatten
Flattens a nested object into a single-level object with dot-notation keys.
Converts nested structures like { user: { name: 'Alice' } } into
{ 'user.name': 'Alice' }. Useful for form data, query parameters,
and database operations.
function flatten(obj: PlainObject, options?: FlattenOptions | undefined): FlatObjectfunction flatten(options?: FlattenOptions | undefined): (obj: PlainObject) => FlatObject// Data-first
const nested = { user: { name: 'Alice', age: 30 } }
flatten(nested)
// => { 'user.name': 'Alice', 'user.age': 30 }
// Custom separator
flatten(nested, { separator: '_' })
// => { 'user_name': 'Alice', 'user_age': 30 }
// Max depth
flatten({ a: { b: { c: 1 } } }, { maxDepth: 1 })
// => { 'a.b': { c: 1 } }
// flatten() works in a pipe. With options, wrap it, because TypeScript
// reads flatten({ separator: '_' }) as the data-first call.
pipe(
nested,
(obj) => flatten(obj, { separator: '_' })
)See also unflatten.
getPath
Safely gets a value at a given path in an object, returning Option.
Navigates nested object structures using a path (array of keys). Returns None if the path runs through a missing or null value, or ends at undefined. A value of null at the end of the path comes back as Some(null). Unlike optional chaining, the result composes with other Option operations.
function getPath<T = unknown>(path: ObjectPath, obj: PlainObject): Option<T>function getPath<T = unknown>(path: ObjectPath): (obj: PlainObject) => Option<T>import { map, unwrapOr } from '@ontologyio/receta/option'
// Data-first
const config = { database: { host: 'localhost', port: 5432 } }
getPath(['database', 'host'], config)
// => Some('localhost')
getPath(['database', 'user'], config)
// => None
getPath(['api', 'key'], config)
// => None
// Composing with Option
pipe(
getPath(['database', 'host'], config),
map(host => `postgres://${host}`),
unwrapOr('postgres://localhost')
)
// Data-last (in pipe)
pipe(
config,
getPath(['database', 'port'])
)See also setPath, updatePath.
mapKeys
Transforms all keys in an object using a mapping function.
Creates a new object with all keys transformed by the provided function. Useful for normalizing keys (camelCase, snake_case), prefixing, or other systematic key transformations.
Note: If multiple keys map to the same new key, later values overwrite earlier ones.
function mapKeys<T extends PlainObject>(obj: T, fn: (key: string, value: T[keyof T]) => string): PlainObjectfunction mapKeys(fn: (key: string, value: any) => string): <T extends PlainObject>(obj: T) => PlainObject// Data-first
const snakeCase = {
first_name: 'Alice',
last_name: 'Smith',
email_address: 'alice@example.com'
}
mapKeys(snakeCase, (key) => key.replace(/_([a-z])/g, (_, c) => c.toUpperCase()))
// => { firstName: 'Alice', lastName: 'Smith', emailAddress: 'alice@example.com' }
// Prefixing keys
mapKeys({ id: 1, name: 'Alice' }, (key) => `user_${key}`)
// => { user_id: 1, user_name: 'Alice' }
// With key and value
mapKeys(obj, (key, value) => typeof value === 'number' ? key.toUpperCase() : key)
// Data-last (in pipe)
pipe(
apiResponse,
mapKeys((key) => toCamelCase(key))
)See also rename, Remeda.mapKeys.
mapValues
Transforms all values in an object using a mapping function.
Creates a new object with all values transformed by the provided function. This is a re-export of Remeda's mapValues for consistency and to enable integration with Result/Option patterns.
function mapValues<T extends PlainObject, U>(obj: T, fn: (value: T[keyof T], key: keyof T) => U): Record<keyof T, U>function mapValues<U>(fn: (value: any, key: string) => U): <T extends PlainObject>(obj: T) => Record<keyof T, U>// Data-first
const prices = { apple: 1.5, banana: 0.5, orange: 2.0 }
mapValues(prices, (price) => price * 1.1) // 10% increase
// => { apple: 1.65, banana: 0.55, orange: 2.2 }
// Type conversion
mapValues({ a: 1, b: 2, c: 3 }, (n) => String(n))
// => { a: '1', b: '2', c: '3' }
// With key and value
mapValues(obj, (value, key) => `${String(key)}: ${value}`)
// Data-last (in pipe)
pipe(
config,
mapValues((v) => normalizeValue(v))
)See also mapKeys, Remeda.mapValues.
mask
Masks an object by keeping only specified keys (allowlist-based filtering).
Security-focused alternative to Remeda's pick. Creates a new object
containing only the allowed keys. Useful for sanitizing data before
sending to clients, logging, or storing.
The key difference from pick is the semantic intent: mask emphasizes
security and data protection, making it clearer when filtering sensitive data.
function mask<T extends PlainObject>(obj: T, allowedKeys: readonly string[]): Partial<T>function mask(allowedKeys: readonly string[]): <T extends PlainObject>(obj: T) => Partial<T>// Data-first
const user = {
id: 1,
email: 'alice@example.com',
passwordHash: 'secret',
creditCard: '4111-1111-1111-1111'
}
mask(user, ['id', 'email'])
// => { id: 1, email: 'alice@example.com' }
// API response sanitization
const safeUser = mask(dbUser, ['id', 'name', 'email', 'role'])
// Data-last (in pipe)
pipe(
user,
mask(['id', 'email', 'name'])
)See also Remeda.pick.
rename
Renames keys in an object according to a mapping.
Creates a new object with renamed keys. If a key isn't in the mapping, it's kept as-is. Useful for transforming API responses, normalizing data, and adapting to different schemas.
function rename<T extends PlainObject>(obj: T, mapping: Record<string, string>): PlainObjectfunction rename(mapping: Record<string, string>): <T extends PlainObject>(obj: T) => PlainObject// Data-first
const user = { firstName: 'Alice', lastName: 'Smith' }
rename(user, { firstName: 'given_name', lastName: 'family_name' })
// => { given_name: 'Alice', family_name: 'Smith' }
// Partial mapping (unmapped keys preserved)
rename({ id: 1, name: 'Alice', age: 30 }, { name: 'fullName' })
// => { id: 1, fullName: 'Alice', age: 30 }
// Data-last (in pipe)
pipe(
apiResponse,
rename({ user_id: 'userId', created_at: 'createdAt' })
)See also mapKeys.
setPath
Immutably sets a value at a given path in an object.
Creates a new object with the value at the specified path updated. Creates intermediate objects/arrays as needed. Returns a shallow copy with only the path modified.
function setPath<T extends PlainObject>(path: ObjectPath, value: unknown, obj: T): Tfunction setPath(path: ObjectPath, value: unknown): <T extends PlainObject>(obj: T) => T// Data-first
const config = { database: { host: 'localhost' } }
setPath(['database', 'port'], 5432, config)
// => { database: { host: 'localhost', port: 5432 } }
// Creating intermediate paths
setPath(['api', 'endpoints', 'users'], '/api/v1/users', {})
// => { api: { endpoints: { users: '/api/v1/users' } } }
// Array indices
setPath(['items', 1], 'updated', { items: ['a', 'b'] })
// => { items: ['a', 'updated'] }
// Data-last (in pipe)
pipe(
config,
setPath(['database', 'host'], 'prod.example.com'),
setPath(['database', 'port'], 5432)
)See also getPath, updatePath.
stripEmpty
Removes all empty values from an object.
By default, removes:
- null and undefined
- Empty strings ('')
- Empty arrays ([])
- Empty objects ({})
This is useful for cleaning API payloads, removing optional fields before sending data, and normalizing objects for comparison or storage.
function stripEmpty(obj: PlainObject, options?: StripEmptyOptions | undefined): PlainObjectfunction stripEmpty(options?: StripEmptyOptions | undefined): (obj: PlainObject) => PlainObject// Basic usage
const input = {
name: 'Alice',
email: '',
tags: [],
metadata: {},
age: null,
verified: false
}
stripEmpty(input)
// => { name: 'Alice', verified: false }
// Keeps non-empty values (0, false are not empty)
stripEmpty({ count: 0, active: false, name: '' })
// => { count: 0, active: false }
// Deep cleaning of nested objects
const nested = {
user: {
name: 'Alice',
bio: '',
settings: {
theme: null,
notifications: []
}
},
metadata: {}
}
stripEmpty(nested)
// => { user: { name: 'Alice' } }
// Customize what to strip
stripEmpty(
{ name: '', tags: [], count: 0 },
{ stripEmptyStrings: false, stripEmptyArrays: true }
)
// => { name: '', count: 0 }
// Preparing API request payload
const formData = {
title: 'New Post',
body: 'Content here',
tags: [],
draft: false,
publishedAt: null
}
const clean = stripEmpty(formData)
fetch('/api/posts', { method: 'POST', body: JSON.stringify(clean) })
// Sends: { title: 'New Post', body: 'Content here', draft: false }
// Shallow stripping (not recursive)
stripEmpty(
{
name: 'Alice',
nested: { bio: '', age: null }
},
{ deep: false }
)
// => { name: 'Alice', nested: { bio: '', age: null } }
// stripEmpty() works in a pipe. With options, wrap it, because TypeScript
// reads stripEmpty({ deep: false }) as the data-first call.
pipe(
apiPayload,
(payload) => stripEmpty(payload)
)See also compact, stripUndefined.
stripUndefined
Removes all undefined values from an object (shallow).
Creates a new object without properties that have undefined values. Useful for cleaning up data before sending to APIs, preventing prototype pollution, and preparing data for serialization.
Note: This is a shallow operation. Use with mapValues for deep cleaning.
function stripUndefined<T extends PlainObject>(obj: T): Partial<T>// Data-first
const input = { name: 'Alice', age: undefined, email: 'alice@example.com' }
stripUndefined(input)
// => { name: 'Alice', email: 'alice@example.com' }
// Preparing API payload
const payload = stripUndefined({
name: formData.name,
email: formData.email,
phone: formData.phone // might be undefined
})
// Data-last (in pipe)
pipe(
formData,
stripUndefined,
validateShape(userSchema)
)See also compact.
transformKeys
Recursively transforms all keys in an object and its nested structures.
This function is useful for converting API responses between different naming conventions (e.g., snake_case from Python APIs to camelCase in JS).
Supports deep transformation of nested objects and arrays of objects.
function transformKeys(obj: PlainObject, transform: KeyTransform, options?: TransformKeysOptions | undefined): PlainObjectfunction transformKeys(transform: KeyTransform, options?: TransformKeysOptions | undefined): (obj: PlainObject) => PlainObject// Convert snake_case API response to camelCase
const apiResponse = {
user_id: 123,
user_profile: {
first_name: 'Alice',
last_name: 'Smith',
contact_info: {
email_address: 'alice@example.com'
}
}
}
transformKeys(apiResponse, 'camelCase')
// => {
// userId: 123,
// userProfile: {
// firstName: 'Alice',
// lastName: 'Smith',
// contactInfo: {
// emailAddress: 'alice@example.com'
// }
// }
// }
// Convert camelCase to snake_case for API request
const jsObject = {
userId: 123,
userProfile: {
firstName: 'Alice',
contactInfo: {
emailAddress: 'alice@example.com'
}
}
}
transformKeys(jsObject, 'snakeCase')
// => {
// user_id: 123,
// user_profile: {
// first_name: 'Alice',
// contact_info: {
// email_address: 'alice@example.com'
// }
// }
// }
// Transform arrays of objects
const users = {
user_list: [
{ first_name: 'Alice', user_id: 1 },
{ first_name: 'Bob', user_id: 2 }
]
}
transformKeys(users, 'camelCase')
// => {
// userList: [
// { firstName: 'Alice', userId: 1 },
// { firstName: 'Bob', userId: 2 }
// ]
// }
// Custom transformation function
transformKeys(
{ hello: 'world', foo: 'bar' },
(key) => key.toUpperCase()
)
// => { HELLO: 'world', FOO: 'bar' }
// Shallow transformation (not recursive)
transformKeys(
{ user_name: 'Alice', user_meta: { created_at: '2024-01-01' } },
'camelCase',
{ deep: false }
)
// => { userName: 'Alice', userMeta: { created_at: '2024-01-01' } }
// Data-last (in pipe)
pipe(
fetchUserData(),
transformKeys('camelCase')
)See also mapKeys.
unflatten
Unflattens a single-level object with dot-notation keys into a nested object.
Reverses the flattening operation, converting { 'user.name': 'Alice' }
back into { user: { name: 'Alice' } }.
function unflatten(obj: FlatObject, options?: UnflattenOptions | undefined): PlainObjectfunction unflatten(options?: UnflattenOptions | undefined): (obj: FlatObject) => PlainObject// Data-first
const flat = { 'user.name': 'Alice', 'user.age': 30 }
unflatten(flat)
// => { user: { name: 'Alice', age: 30 } }
// Custom separator
unflatten({ 'user_name': 'Alice' }, { separator: '_' })
// => { user: { name: 'Alice' } }
// Array indices
unflatten({ 'items.0': 'a', 'items.1': 'b' })
// => { items: ['a', 'b'] }
// unflatten() works in a pipe. With options, wrap it, because TypeScript
// reads unflatten({ separator: '_' }) as the data-first call.
pipe(
flat,
(obj) => unflatten(obj)
)See also flatten.
updatePath
Immutably updates a value at a given path using a function.
Retrieves the current value at the path, applies the update function, and sets the new value. If the path doesn't exist, the object is returned unchanged.
function updatePath<T extends PlainObject, V>(path: ObjectPath, fn: (value: V) => V, obj: T): Tfunction updatePath<V>(path: ObjectPath, fn: (value: V) => V): <T extends PlainObject>(obj: T) => T// Data-first
const user = { profile: { views: 10 } }
updatePath(['profile', 'views'], (n: number) => n + 1, user)
// => { profile: { views: 11 } }
// Path doesn't exist - returns original
updatePath(['profile', 'likes'], (n: number) => n + 1, user)
// => { profile: { views: 10 } }
// Transforming nested data
updatePath(['database', 'host'], (host: string) => host.toUpperCase(), config)
// Data-last (in pipe)
pipe(
user,
updatePath(['profile', 'views'], (n: number) => n + 1),
updatePath(['profile', 'lastSeen'], () => new Date())
)See also getPath, setPath.
validateShape
Validates an object against a schema, returning Result.
Checks that an object matches a given shape/schema. Returns Ok if valid, Err with details if invalid. Useful for runtime type checking, API validation, and form validation.
function validateShape<T extends PlainObject>(obj: unknown, schema: ObjectSchema<T>): Result<T, ObjectError>function validateShape<T extends PlainObject>(schema: ObjectSchema<T>): (obj: unknown) => Result<T, ObjectError>Fails with ObjectError.
import { map } from '@ontologyio/receta/result'
// Define schema with predicates
const userSchema = {
id: (v: unknown): v is number => typeof v === 'number',
name: (v: unknown): v is string => typeof v === 'string',
email: (v: unknown): v is string =>
typeof v === 'string' && v.includes('@')
}
// Data-first
const input = { id: 1, name: 'Alice', email: 'alice@example.com' }
validateShape(input, userSchema)
// => Ok({ id: 1, name: 'Alice', email: 'alice@example.com' })
const invalid = { id: '1', name: 'Alice', email: 'invalid' }
validateShape(invalid, userSchema)
// => Err({ code: 'VALIDATION_ERROR', message: 'Validation failed for key "id"', path: ['id'] })
// Data-last (in pipe)
pipe(
apiResponse,
stripUndefined,
(obj) => validateShape(obj, userSchema),
map(user => processUser(user))
)
// With Predicate module
import { isString, isNumber } from '@ontologyio/receta/predicate'
const profileSchema = {
id: isNumber,
name: isString,
age: (v: unknown): v is number => isNumber(v) && v >= 0 && v <= 150
}See also Validation.
Types
DeepMergeOptions
Options for deep merging objects.
interface DeepMergeOptions {
readonly arrayStrategy?: 'replace' | 'concat' | 'merge'
readonly customMerge?: (key: string, target: unknown, source: unknown) => unknown
}DeepPartial
Makes all properties of T deeply partial (optional).
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}FlatObject
Flattened object with dot-notation keys.
type FlatObject = Record<string, unknown>const flat: FlatObject = {
'user.name': 'Alice',
'user.age': 30
}FlattenOptions
Options for flattening objects.
interface FlattenOptions {
readonly separator?: string
readonly maxDepth?: number
readonly flattenArrays?: boolean
}KeysOfType
Keys of T that have values of type V.
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]KeyTransform
Type of key transformation to apply.
type KeyTransform = 'camelCase' | 'snakeCase' | 'kebabCase' | 'pascalCase' | ((key: string) => string)ObjectError
Error type for object operations.
interface ObjectError {
readonly code: 'PATH_NOT_FOUND' | 'INVALID_PATH' | 'VALIDATION_ERROR' | 'CIRCULAR_REFERENCE'
readonly message: string
readonly path?: ObjectPath
readonly cause?: unknown
}ObjectPath
Represents a path into a nested object as an array of keys.
type ObjectPath = readonly (string | number)[]const path: ObjectPath = ['user', 'address', 'city']ObjectSchema
Schema for object validation.
type ObjectSchema<T extends PlainObject> = {
readonly [K in keyof T]: Validator<T[K]> | ObjectSchema<any>
}PathValue
Get the type at a given path in an object.
type PathValue<T, P extends readonly any[]> = P extends readonly [infer First, ...infer Rest]
? First extends keyof T
? Rest extends readonly any[]
? PathValue<T[First], Rest>
: T[First]
: never
: TPlainObject
A plain object (not an array, not null, not a primitive).
type PlainObject = Record<string, unknown>StripEmptyOptions
Options for stripEmpty operation.
interface StripEmptyOptions {
readonly stripEmptyStrings?: boolean
readonly stripEmptyArrays?: boolean
readonly stripEmptyObjects?: boolean
readonly deep?: boolean
}TransformKeysOptions
Options for transformKeys operation.
interface TransformKeysOptions {
readonly deep?: boolean
readonly transformArrays?: boolean
}UnflattenOptions
Options for unflattening objects.
interface UnflattenOptions {
readonly separator?: string
}