On This Page
Object Utilities
The Object utilities provide a set of functions for working with objects in JavaScript. These functions help in manipulating, transforming, and querying objects efficiently.
Addressing a value by string path — get, set, unset, and the keyed [#id] grammar the changesets on this page emit — lives in Path Utilities.
Functions
keys
function keys(obj)Return keys from an object.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to get keys from |
Returns
An array of the object’s keys, or undefined if the input is not an object.
Example
import { keys } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3 };console.log(keys(obj)); // ['a', 'b', 'c']values
function values(obj)Return values from an object.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to get values from |
Returns
An array of the object’s values, or undefined if the input is not an object.
Example
import { values } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3 };console.log(values(obj)); // [1, 2, 3]filterObject
function filterObject(obj, callback)Filter an object based on a callback function.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to filter |
| callback | function | The callback function to test each key-value pair |
Returns
A new object with the key-value pairs that passed the test.
Example
import { filterObject } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3, d: 4 };const result = filterObject(obj, (value) => value % 2 === 0);console.log(result); // { b: 2, d: 4 }mapObject
function mapObject(obj, callback)Transform an object’s values based on a callback function.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to transform |
| callback | function | The callback function to transform each value |
Returns
A new object with transformed values.
Example
import { mapObject } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3 };const result = mapObject(obj, (value) => value * 2);console.log(result); // { a: 2, b: 4, c: 6 }trackWrites
function trackWrites(value, callback, { strategy = 'auto', returnPaths = true, keyed = true, fields = elementKey.config.fields, onWrite, clone, equality } = {})Runs a callback against a value and reports whether the callback changed it, with the changed fields as dot paths. Useful for running a user callback against a data structure and determining mutations.
Strategy - The default strategy ‘auto’ will use
snapshotfor small data structures andproxyfor larger. Snapshot improves debugging by passing you a real copy, whileproxypreserves performance at the cost of passing you aproxyObjectinside your callback. Keyed paths (the default) come from the snapshot diff, so when they are wanted'auto'yields tosnapshoteven for a large value — an explicit'proxy'strategy keeps the no-clone path at the cost of positional paths.
Parameters
| Name | Type | Description |
|---|---|---|
| value | any | The value the callback may change |
| callback | function | Receives the value (or its tracked wrapper) |
| options | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| strategy | string | ‘auto’ | 'auto', 'snapshot', or 'proxy' |
| returnPaths | boolean | true | Return changed fields as dot paths. Pass false to skip collection on hot paths that only read changed |
| keyed | boolean | true | Id-address paths for keyed arrays (todos[#id].complete instead of todos.0.complete). Forces the snapshot strategy when paths are wanted, since keyed paths come from the snapshot diff. Pass false to keep the proxy’s no-clone behaviour on a large value |
| fields | string[] | elementKey.config.fields |
Identity fields for keyed paths, first present wins |
| onWrite | function | undefined | (path, target, key) fires per observed write with the key path from the root. Implies the proxy strategy under 'auto', so its paths are positional |
| clone | function | clone | Clone used for snapshots |
| equality | function | isEqual | Equality deciding what counts as a change, across the path diff, the returnPaths: false fast path, and exotic snapshots |
Returns
{ changed, paths, result } — whether the value changed, the changed fields as dot paths, and the callback’s return value.
Notes
paths is a covering set resolvable through get. The snapshot strategy reports net leaf differences via detectChanges and id-addresses keyed arrays by default (field[#id]). The proxy strategy reports the paths written (pruned so a written parent subsumes its children) and these are always positional — the proxy only sees the index a write went through. A wholesale change to a non-container value reports path ''. Under the proxy strategy the tracked wrapper is only valid inside the callback — using one after it returns throws.
Example
import { trackWrites, get } from '@semantic-ui/utils';
const doc = { meta: { count: 0 } };const { changed, paths } = trackWrites(doc, (value) => { value.meta.count++;});console.log(changed); // trueconsole.log(paths); // ['meta.count']
// paths resolve against the value, e.g. for state syncpaths.forEach((path) => sync(path, get(doc, path)));
// keyed paths by default — a field edit across a collection reads back per// record, not by index, with zero configurationconst db = { todos: [{ id: 'a', complete: false }, { id: 'b', complete: false }] };const completeAll = trackWrites(db, (d) => { for (const todo of d.todos) { todo.complete = true; }});console.log(completeAll.paths); // ['todos[#a].complete', 'todos[#b].complete']
// the proxy strategy is the positional opt-out (no element identity)trackWrites(db, mutator, { strategy: 'proxy' }); // paths like ['todos.0.complete']
// skip path collection on hot pathstrackWrites(doc, mutator, { returnPaths: false }); // { changed, result }trackReads
function trackReads(value, callback, { returnPaths = true, keyed = true, fields = elementKey.config.fields, onRead } = {})Runs a callback against a value and reports which paths it read — the read companion to trackWrites. Where trackWrites answers “what did this change”, trackReads answers “what did this depend on”: collecting a computed function’s dependencies, deriving a memoization key from the values it touched, auditing least-privilege access, or learning what to prefetch.
Proxy only - Reads are observable only through a proxy (there is no before/after to diff, so unlike
trackWritesthere is no snapshot strategy). The value is wrapped read-only: the callback may read any depth, but a write through the wrapper throws, so the input is never mutated. The wrapper is only valid inside the callback — using one after it returns throws.
Parameters
| Name | Type | Description |
|---|---|---|
| value | any | The value the callback reads from |
| callback | function | Receives the value (or its read-only tracked wrapper) and returns anything |
| options | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| returnPaths | boolean | true | Return the read paths. Pass false to skip collection and rely on onRead |
| keyed | boolean | true | Id-address paths for keyed arrays (todos[#id].done instead of todos.0.done), the same convention as trackWrites. Pass false for positional paths |
| fields | string[] | elementKey.config.fields |
Identity fields for keyed paths, first present wins |
| onRead | function | undefined | (path, type, target, key) fires per read with its type ('value', 'has', or 'structure') — the unpruned live stream for dependency collectors and auditors |
Returns
{ reads, structure, result } — the value paths read, the container shapes read, and the callback’s return value.
Notes
Two dependency kinds come back, kept apart because they invalidate on different writes:
reads— value paths (todos[#id].done), resolvable throughget, pruned so a deeper read subsumes its ancestors. Pairs withdetectChangeschanged.structure— container paths whose shape was read: an array’s.length, iteration, spread, orObject.keys. Pairs withdetectChangesadded/removed.
Surfacing structure separately is the array-growth case: reading an array’s length leaves no value path behind, so a value-only dependency set would silently miss a push. Reading a method (.reduce) is not itself a dependency — the property reads that method then performs are. An exotic (Date/Map/Set/RegExp) is a single read with no recursion into its internals. A frozen subtree is immutable, so it has no dependencies and is read only at its boundary.
Example
import { trackReads, get } from '@semantic-ui/utils';
const state = { todos: [{ id: 'a', done: false }, { id: 'b', done: true }] };const { reads, structure } = trackReads(state, (value) => value.todos.map((todo) => todo.done));console.log(reads); // ['todos[#a].done', 'todos[#b].done']console.log(structure); // ['todos']
// reads resolve back through get(), e.g. for a memoization key — and the keyed// paths survive a reorder of the listreads.map((path) => get(state, path)); // [false, true]
// pair with the write side: re-run when a read value changes (changed) or when// a read container grows or shrinks (added/removed)// reads ↔ detectChanges.changed// structure ↔ detectChanges.added / removed
// onRead streams each read live, typed, for dependency collectorstrackReads(state, (value) => value.todos.length, { onRead: (path, type) => console.log(type, path), // 'value' todos, 'structure' todos});
// positional opt-out, and hot-path streaming without collectiontrackReads(state, reader, { keyed: false }); // paths like ['todos.0.done']trackReads(state, reader, { returnPaths: false, onRead });detectChanges
function detectChanges(before, after, { keyed = true, fields = elementKey.config.fields, equality = isEqual, ignoreKeys = null, collapseKeys = null,} = {})Structural diff between two values, reported as dot paths from before to after — added (in after only), removed (in before only), and changed (in both, different values).
Parameters
| Name | Type | Description |
|---|---|---|
| before | any | The value to diff from |
| after | any | The value to diff to |
| options | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| keyed | boolean | true | Diff arrays of uniquely-keyed objects by element identity instead of by index. Pass false for the legacy positional walk |
| fields | string[] | elementKey.config.fields |
Identity fields a keyed element is matched on |
| equality | function | isEqual |
Comparator deciding whether two leaf values count as changed. The same option trackWrites takes — pass a looser rule (==, an epsilon) for loosely-typed data |
| ignoreKeys | string[] | null |
Key names dropped from the result at any depth — a volatile or local-only field (updatedAt, a client annotation) never reaches the changeset |
| collapseKeys | string[] | null |
Key names diffed as one whole value, never descended into, at any depth. The key reports as a single path when it changes |
Returns
{ added, removed, changed } — arrays of dot paths.
Notes
Objects recurse to leaf paths. Values that can’t be walked (Map, Set, Date, class instances) compare by deep equality and report their own path. Differing non-container roots report path ''.
By default, an array whose elements are each uniquely keyed (see elementKey) diffs by identity instead of position. It emits field[#identity] paths — field[#z] for a whole-element add, remove, or replace, field[#b].qty for a field change — so a prepend or reorder is one add by key, not a positional cascade of index rewrites. An id may be any string without ] — emails and compound ids included, users[#jack@semantic-ui.com].role emits and parses back. Any array that isn’t cleanly keyed (a scalar or unkeyed element, a duplicate key, or a key value carrying ]) falls back to the positional walk, so an emitted keyed path always parses back through get / set / unset. Pass { keyed: false } for the legacy positional output, where arrays diff by index and a shifted array reports every moved position.
collapseKeys gives a plain-object key the same whole-value treatment a Map or Date already gets — the walk stops at the key and reports it as one path instead of descending. This is for a subtree whose own keys aren’t addressable wire paths, like a map keyed by dynamic contacts[#id].field strings that would mis-parse if emitted nested. A wholesale add or remove of the key still reports as add or remove, and a change anywhere inside reports the key as changed. ignoreKeys is the complement — those keys leave the changeset entirely.
Example
import { detectChanges } from '@semantic-ui/utils';
const before = { name: 'a', temp: true, count: 1 };const after = { name: 'b', count: 1, nickname: 'al' };
console.log(detectChanges(before, after));// { added: ['nickname'], removed: ['temp'], changed: ['name'] }
// arrays of keyed objects diff by identity — a prepend plus an edit is one addconst cartBefore = { lineItems: [{ id: 'a', qty: 1 }, { id: 'b', qty: 1 }] };const cartAfter = { lineItems: [{ id: 'z', qty: 9 }, { id: 'a', qty: 1 }, { id: 'b', qty: 5 }] };console.log(detectChanges(cartBefore, cartAfter));// { added: ['lineItems[#z]'], removed: [], changed: ['lineItems[#b].qty'] }
// scalar arrays (no element identity) fall back to the positional walkconsole.log(detectChanges({ items: [1, 2, 3] }, { items: [1, 9] }));// { added: [], removed: ['items.2'], changed: ['items.1'] }
// { keyed: false } restores the positional cascade for keyed objects tooconsole.log(detectChanges(cartBefore, cartAfter, { keyed: false }));// { added: ['lineItems.2'], removed: [], changed: ['lineItems.0.id', 'lineItems.0.qty', 'lineItems.1.id'] }
// equality swaps the comparator — '1' and 1 are not a change under ==console.log(detectChanges({ a: 1 }, { a: '1' }, { equality: (x, y) => x == y }));// { added: [], removed: [], changed: [] }
// ignoreKeys drops a field at any depth; collapseKeys reports a subtree wholeconsole.log(detectChanges( { name: 'a', _overrides: { 'contacts[#1].field': true }, updatedAt: 1 }, { name: 'b', _overrides: { 'contacts[#1].field': false }, updatedAt: 2 }, { ignoreKeys: ['updatedAt'], collapseKeys: ['_overrides'] },));// { added: [], removed: [], changed: ['name', '_overrides'] }extend
function extend(obj, ...sources)Extend an object with properties from other objects, properly handling getter/setters.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The target object to extend |
| sources | …object | One or more source objects |
Returns
The extended object.
Example
import { extend } from '@semantic-ui/utils';
const obj1 = { a: 1, b: 2 };const obj2 = { c: 3 };const obj3 = { d: 4 };const result = extend(obj1, obj2, obj3);console.log(result); // { a: 1, b: 2, c: 3, d: 4 }assignInPlace
function assignInPlace(target, source, { preserveExistingKeys = false, preserveGetters = false, returnChanged = false } = {})Mutates the target object in place so its contents match the source, without replacing the object reference. Deletes keys not present in source (unless preserveExistingKeys is true), then assigns all source properties.
Parameters
| Name | Type | Description |
|---|---|---|
| target | object | The object to update in place |
| source | object | The object whose properties to apply |
| options | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| preserveExistingKeys | boolean | false | Keep keys in target that are not in source |
| preserveGetters | boolean | false | Skip own getter descriptors when deleting keys not in source. Useful when target carries computed properties that shouldn’t be torn down by syncs |
| returnChanged | boolean | false | Return whether any properties changed instead of the target |
Returns
The mutated target object. If returnChanged is true, returns a boolean indicating whether any properties were added, removed, or changed.
Example
import { assignInPlace } from '@semantic-ui/utils';
const config = { host: 'localhost', port: 3000, debug: true };assignInPlace(config, { host: 'production.app', port: 443 });console.log(config); // { host: 'production.app', port: 443 } — debug removed
// Preserve existing keysconst settings = { theme: 'light', lang: 'en' };assignInPlace(settings, { theme: 'dark', fontSize: 14 }, { preserveExistingKeys: true });console.log(settings); // { theme: 'dark', lang: 'en', fontSize: 14 }
// Preserve computed propertiesconst view = { name: 'Alice' };Object.defineProperty(view, 'greeting', { get() { return `Hi, ${this.name}`; }, enumerable: true });assignInPlace(view, { name: 'Bob' }, { preserveGetters: true });console.log(view.greeting); // 'Hi, Bob' — getter intact
// Detect changesconst state = { count: 5 };assignInPlace(state, { count: 5 }, { returnChanged: true }); // falseassignInPlace(state, { count: 10 }, { returnChanged: true }); // truedeepExtend
function deepExtend(obj, ...sources, options)Deep extends an object with properties from other sources, recursively merging nested plain objects and cloning non-plain objects.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The target object to extend |
| sources | …object | One or more source objects to merge from |
| options | object | Optional configuration object |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| preserveNonCloneable | boolean | true | Keep a class instance as is instead of flattening it to a plain object |
| preserveDOM | boolean | true | Keep a DOM node as is instead of walking it |
Returns
The modified target object with deep merged properties.
Example
import { deepExtend } from '@semantic-ui/utils';
const target = { user: { name: 'Alice', age: 30 }, settings: { theme: 'light' }};
const source = { user: { role: 'admin' }, settings: { notifications: true }};
deepExtend(target, source);console.log(target);// {// user: { name: 'Alice', age: 30, role: 'admin' },// settings: { theme: 'light', notifications: true }// }pick
function pick(obj, ...keys)Create a new object with only the specified keys from the original object.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The source object |
| keys | …string | The keys to pick |
Returns
A new object with only the specified keys.
Example
import { pick } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3, d: 4 };const result = pick(obj, 'a', 'c');console.log(result); // { a: 1, c: 3 }arrayFromObject
function arrayFromObject(obj)Convert an object to an array of key-value pairs.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to convert |
Returns
An array of key-value pair objects.
Example
import { arrayFromObject } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3 };const result = arrayFromObject(obj);console.log(result); // [{ key: 'a', value: 1 }, { key: 'b', value: 2 }, { key: 'c', value: 3 }]proxyObject
function proxyObject(sourceObj = noop, referenceObj = {})Create a proxy object that combines properties from a source object and a reference object.
Parameters
| Name | Type | Description |
|---|---|---|
| sourceObj | function | A function that returns the source object |
| referenceObj | object | The reference object |
Returns
A proxy object combining properties from both objects.
Example
import { proxyObject } from '@semantic-ui/utils';
const source = () => ({ a: 1, b: 2 });const reference = { c: 3 };const proxy = proxyObject(source, reference);console.log(proxy.a); // 1console.log(proxy.c); // 3onlyKeys
function onlyKeys(obj, keysToKeep)Create a new object with only the specified keys from the original object.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The source object |
| keysToKeep | array | The keys to keep in the new object |
Returns
A new object with only the specified keys.
Example
import { onlyKeys } from '@semantic-ui/utils';
const obj = { a: 1, b: 2, c: 3, d: 4 };const result = onlyKeys(obj, ['a', 'c']);console.log(result); // { a: 1, c: 3 }hasProperty
function hasProperty(obj, prop)Check if an object has a non-inherited property.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to check |
| prop | string | The property name to check for |
Returns
True if the object has the property, false otherwise.
Example
import { hasProperty } from '@semantic-ui/utils';
const obj = { a: 1, b: 2 };console.log(hasProperty(obj, 'a')); // trueconsole.log(hasProperty(obj, 'toString')); // falsereverseKeys
function reverseKeys(obj)Reverse the keys and values of an object.
Parameters
| Name | Type | Description |
|---|---|---|
| obj | object | The object to reverse |
Returns
A new object with reversed keys and values.
Example
import { reverseKeys } from '@semantic-ui/utils';
const obj = { a: '1', b: ['2', '3'] };const result = reverseKeys(obj);console.log(result); // { '1': 'a', '2': 'b', '3': 'b' }These object utilities provide a robust set of tools for working with objects in JavaScript, enhancing productivity and code readability.
weightedObjectSearch
function weightedObjectSearch(query, objectArray, { returnMatches = false, matchAllWords = true, propertiesToMatch = [] } = {})Performs a weighted search across an array of objects, with matches prioritized by where they occur in the text.
Search Priority Results are sorted by match quality, with highest priority given to exact start of string matches (e.g., searching “cat” matching “category”), followed by word-start matches (e.g., “category” in “my category”), then substring matches anywhere, and finally partial word matches. When searching multiple words, matches are weighted by how many words were found in the text.
Parameters
| Name | Type | Description |
|---|---|---|
| query | string | The search query |
| objectArray | array | Array of objects to search |
| options | object | Search configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| returnMatches | boolean | false | Include match details in results |
| matchAllWords | boolean | true | Require all words to match |
| propertiesToMatch | array | [] | Properties to search within objects |
Understanding Matches When
returnMatchesistrue, each result is a spread copy of the original object with an addedmatchesarray property. The original objects are never mutated. Each match has the shape{ field, type, score, value }wheretypeis one of'startsWith','wordStartsWith','anywhere', or'anyWord'.
Returns
Array of matching objects sorted by relevance. When returnMatches is true, each result is a shallow copy with an additional matches property containing an array of match details.
Example
import { weightedObjectSearch } from '@semantic-ui/utils';
const items = [ { title: 'Hello World', desc: 'A greeting' }, { title: 'World News', desc: 'Current events' }];
// Basic searchconst results = weightedObjectSearch('world', items, { propertiesToMatch: ['title', 'desc']});
// With match details (does not mutate originals)const detailed = weightedObjectSearch('world', items, { propertiesToMatch: ['title', 'desc'], returnMatches: true,});// detailed[0].matches → [{ field: 'title', type: 'startsWith', score: 1, value: 'World News' }]