receta

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.

Signature
function compact<T extends PlainObject>(obj: T): Partial<T>
Example
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.

Data-first
function deepMerge(objects: readonly PlainObject[], options?: DeepMergeOptions | undefined): PlainObject
Data-last
function deepMerge(options?: DeepMergeOptions | undefined): (objects: readonly PlainObject[]) => PlainObject
Example
// 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.

Data-first
function filterKeys<T extends PlainObject>(obj: T, predicate: (key: string) => boolean): Partial<T>
Data-last
function filterKeys(predicate: (key: string) => boolean): <T extends PlainObject>(obj: T) => Partial<T>
Example
// 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.

Data-first
function filterValues<T extends PlainObject>(obj: T, predicate: (value: T[keyof T], key: keyof T) => boolean): Partial<T>
Data-last
function filterValues(predicate: (value: any, key: string) => boolean): <T extends PlainObject>(obj: T) => Partial<T>
Example
// 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.

Data-first
function flatten(obj: PlainObject, options?: FlattenOptions | undefined): FlatObject
Data-last
function flatten(options?: FlattenOptions | undefined): (obj: PlainObject) => FlatObject
Example
// 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.

Data-first
function getPath<T = unknown>(path: ObjectPath, obj: PlainObject): Option<T>
Data-last
function getPath<T = unknown>(path: ObjectPath): (obj: PlainObject) => Option<T>
Example
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.

Data-first
function mapKeys<T extends PlainObject>(obj: T, fn: (key: string, value: T[keyof T]) => string): PlainObject
Data-last
function mapKeys(fn: (key: string, value: any) => string): <T extends PlainObject>(obj: T) => PlainObject
Example
// 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.

Data-first
function mapValues<T extends PlainObject, U>(obj: T, fn: (value: T[keyof T], key: keyof T) => U): Record<keyof T, U>
Data-last
function mapValues<U>(fn: (value: any, key: string) => U): <T extends PlainObject>(obj: T) => Record<keyof T, U>
Example
// 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.

Data-first
function mask<T extends PlainObject>(obj: T, allowedKeys: readonly string[]): Partial<T>
Data-last
function mask(allowedKeys: readonly string[]): <T extends PlainObject>(obj: T) => Partial<T>
Example
// 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.

Data-first
function rename<T extends PlainObject>(obj: T, mapping: Record<string, string>): PlainObject
Data-last
function rename(mapping: Record<string, string>): <T extends PlainObject>(obj: T) => PlainObject
Example
// 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.

Data-first
function setPath<T extends PlainObject>(path: ObjectPath, value: unknown, obj: T): T
Data-last
function setPath(path: ObjectPath, value: unknown): <T extends PlainObject>(obj: T) => T
Example
// 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.

Data-first
function stripEmpty(obj: PlainObject, options?: StripEmptyOptions | undefined): PlainObject
Data-last
function stripEmpty(options?: StripEmptyOptions | undefined): (obj: PlainObject) => PlainObject
Example
// 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.

Signature
function stripUndefined<T extends PlainObject>(obj: T): Partial<T>
Example
// 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.

Data-first
function transformKeys(obj: PlainObject, transform: KeyTransform, options?: TransformKeysOptions | undefined): PlainObject
Data-last
function transformKeys(transform: KeyTransform, options?: TransformKeysOptions | undefined): (obj: PlainObject) => PlainObject
Example
// 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' } }.

Data-first
function unflatten(obj: FlatObject, options?: UnflattenOptions | undefined): PlainObject
Data-last
function unflatten(options?: UnflattenOptions | undefined): (obj: FlatObject) => PlainObject
Example
// 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.

Data-first
function updatePath<T extends PlainObject, V>(path: ObjectPath, fn: (value: V) => V, obj: T): T
Data-last
function updatePath<V>(path: ObjectPath, fn: (value: V) => V): <T extends PlainObject>(obj: T) => T
Example
// 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.

Data-first
function validateShape<T extends PlainObject>(obj: unknown, schema: ObjectSchema<T>): Result<T, ObjectError>
Data-last
function validateShape<T extends PlainObject>(schema: ObjectSchema<T>): (obj: unknown) => Result<T, ObjectError>

Fails with ObjectError.

Example
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.

Signature
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).

Signature
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

FlatObject

Flattened object with dot-notation keys.

Signature
type FlatObject = Record<string, unknown>
Example
const flat: FlatObject = {
  'user.name': 'Alice',
  'user.age': 30
}

FlattenOptions

Options for flattening objects.

Signature
interface FlattenOptions {
  readonly separator?: string

  readonly maxDepth?: number

  readonly flattenArrays?: boolean
}

KeysOfType

Keys of T that have values of type V.

Signature
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never
}[keyof T]

KeyTransform

Type of key transformation to apply.

Signature
type KeyTransform = 'camelCase' | 'snakeCase' | 'kebabCase' | 'pascalCase' | ((key: string) => string)

ObjectError

Error type for object operations.

Signature
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.

Signature
type ObjectPath = readonly (string | number)[]
Example
const path: ObjectPath = ['user', 'address', 'city']

ObjectSchema

Schema for object validation.

Signature
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.

Signature
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
  : T

PlainObject

A plain object (not an array, not null, not a primitive).

Signature
type PlainObject = Record<string, unknown>

StripEmptyOptions

Options for stripEmpty operation.

Signature
interface StripEmptyOptions {
  readonly stripEmptyStrings?: boolean

  readonly stripEmptyArrays?: boolean

  readonly stripEmptyObjects?: boolean

  readonly deep?: boolean
}

TransformKeysOptions

Options for transformKeys operation.

Signature
interface TransformKeysOptions {
  readonly deep?: boolean

  readonly transformArrays?: boolean
}

UnflattenOptions

Options for unflattening objects.

Signature
interface UnflattenOptions {
  readonly separator?: string
}

On this page