Skip to content

Object helpers ​

Seven helpers for reshaping objects, including the two cases ?. and if (!value) get wrong.

Open in demo
Open in demo

Import ​

ts
import { cleanObject, get, isEmpty, omit, pick } from '@shared/utils/object'
import { deepEqual } from '@shared/utils/deepEqual'
import { deepMerge } from '@shared/utils/deepMerge'

pick ​

ts
function pick<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Pick<T, K>
ts
pick(user, ['id', 'email'])

Absent keys are not invented, so this never adds an undefined property for JSON.stringify to drop or a server to reject.

omit ​

ts
function omit<T extends object, K extends keyof T>(source: T, keys: readonly K[]): Omit<T, K>
ts
omit(form, ['passwordConfirmation'])

get ​

ts
function get<T = unknown>(source: unknown, path: string): T | undefined
function get<T>(source: unknown, path: string, fallback: T): T

Reads a nested value by path without throwing on a missing link. Both notations work:

ts
get(response, 'data.items[0].id')
get(response, 'data.items.0.id')
get(response, 'data.total', 0) // 0 when absent

This is for when the path is data — a column definition, a runtime sort key, a config string — where optional chaining cannot reach because the path is not known when you write the code.

cleanObject ​

ts
function cleanObject<T extends object>(source: T, options?: CleanObjectOptions): Partial<T>

interface CleanObjectOptions {
  removeEmptyStrings?: boolean
  removeEmptyCollections?: boolean
}
ts
cleanObject({ q: '', page: 0, role: null }) // { page: 0 }

Drops null and undefined, and by default '' too — an empty filter input is not a filter, so a filter bar sends ?status=active rather than ?q=&role=null.

0 and false are kept. Dropping them would lose page=0 and archived=false, which is the bug this avoids.

removeEmptyStrings defaults to true, removeEmptyCollections to false.

isEmpty ​

ts
function isEmpty(value: unknown): boolean
ValueisEmpty
null, undefinedtrue
''true
[], {}true
an empty Map or Settrue
0, false, NaNfalse
any Datefalse
ts
isEmpty(0) // false
isEmpty({}) // true

The last two rows are the point. if (!value) treats 0 and false as absent, which is how a quantity of zero disappears from a form.

deepEqual ​

ts
function deepEqual(a: unknown, b: unknown): boolean
ts
import { deepEqual } from '@shared/utils/deepEqual'

Structural comparison. useFormDirty is built on it.

deepMerge ​

ts
function deepMerge<T extends object>(target: T, source: DeepPartial<NoInfer<T>>): T
ts
import { deepMerge } from '@shared/utils/deepMerge'

Merges nested objects; arrays are replaced, not concatenated. It is what useConfig's mergeConfig and usePageConfig use.