Awesome
<img alt="wild-wild-utils logo" src="https://raw.githubusercontent.com/ehmicky/design/main/wild-wild-utils/wild-wild-utils.svg?sanitize=true" width="700"/>π€ Functional utilities using object property paths with wildcards and regexps. π΅
Available functional methods include:
- πΊοΈ Mapping:
map()
,flatten()
- π Merging/concatenating:
merge()
,push()
,unshift()
- βοΈ Finding:
find()
- β Filtering:
pick()
,include()
,exclude()
Unlike similar libraries, object properties can be get/set using
dot-delimited paths,
wildcards,
regexps,
slices and
unions. It is built on top
of wild-wild-path
.
Hire me
Please reach out if you're looking for a Node.js API or CLI engineer (11 years of experience). Most recently I have been Netlify Build's and Netlify Plugins' technical lead for 2.5 years. I am available for full-time remote positions.
Install
npm install wild-wild-utils
This package works in both Node.js >=18.18.0 and browsers.
This is an ES module. It must be loaded using
an import
or import()
statement,
not require()
. If TypeScript is used, it must be configured to
output ES modules,
not CommonJS.
API
Methods
map(target, query, mapFunction, options?)
target
: Target
query
: Query
mapFunction
: (value) => value
options
: Options?
Return value: Target
Use a mapFunction()
to modify any property matching the query
.
const target = { user: { firstName: 'Alice', lastName: 'Smith' } }
map(target, 'user.*', (userProp) => userProp.toLowerCase())
// { user: { firstName: 'alice', lastName: 'smith' } }
merge(target, query, value, options?)
target
: Target
query
: Query
value
: any
options
: Options?
Return value: Target
Deeply merge an object value
with each object property matching the query
.
If one of these properties is not an object, it is overridden instead.
Any object in value
can change
the merge mode using a _merge
property with value
"deep"
(default),
"shallow"
,
"set"
or
"delete"
.
Arrays
can be merged using objects in value
where the keys are the
array indices. Items can
be updated,
merged,
added,
inserted,
appended,
prepended or
deleted.
const target = {
userOne: { names: ['Alice', 'Smith'], settings: { deleted: true } },
userTwo: { names: ['John', 'Doe'], settings: { deleted: false } },
}
merge(target, '*', { age: 72, settings: { admin: true } })
// {
// userOne: {
// names: ['Alice', 'Smith'],
// settings: { deleted: true, admin: true },
// age: 72,
// },
// userTwo: {
// names: ['John', 'Doe'],
// settings: { deleted: false, admin: true },
// age: 72,
// },
// }
merge(target, '*', { age: 72, settings: { admin: true }, _merge: 'shallow' })
// {
// userOne: {
// names: [ 'Alice', 'Smith' ],
// settings: { admin: true },
// age: 72,
// },
// userTwo: {
// names: [ 'John', 'Doe' ],
// settings: { admin: true },
// age: 72,
// },
// }
merge(target, '*', { names: { 1: 'Red' } })
// {
// userOne: {
// names: ['Alice', 'Red'],
// settings: { deleted: true },
// age: 72,
// },
// userTwo: {
// names: ['John', 'Red'],
// settings: { deleted: false },
// age: 72,
// },
// }
push(target, query, values, options?)
target
: Target
query
: Query
values
: any[]
options
: Options?
Return value: Target
Concatenate an array of values
with each array property matching the query
.
If one of these properties is not an array, it is overridden instead.
const target = {
userOne: { firstName: 'Alice', colors: ['red'] },
userTwo: { firstName: 'John', colors: ['blue'] },
}
push(target, '*.colors', ['yellow', 'silver'])
// {
// userOne: { firstName: 'Alice', colors: ['red', 'yellow', 'silver'] },
// userTwo: { firstName: 'John', colors: ['blue', 'yellow', 'silver'] },
// }
unshift(target, query, values, options?)
target
: Target
query
: Query
values
: any[]
options
: Options?
Return value: Target
Like push()
but concatenates at the
beginning of each property instead of at the end.
const target = {
userOne: { firstName: 'Alice', colors: ['red'] },
userTwo: { firstName: 'John', colors: ['blue'] },
}
unshift(target, '*.colors', ['yellow', 'silver'])
// {
// userOne: { firstName: 'Alice', colors: ['yellow', 'silver', 'red'] },
// userTwo: { firstName: 'John', colors: ['yellow', 'silver', 'blue'] },
// }
find(target, query, testFunction, options?)
target
: Target
query
: Query
testFunction
: (value) => boolean
options
: Options?
Return value: any
Return the first property that matches the query
and that returns true
with
the testFunction()
.
const target = {
userOne: { firstName: 'Alice', colors: ['red'] },
userTwo: { firstName: 'John', colors: ['blue'] },
}
find(target, '*.firstName', (firstName) => firstName !== 'John') // 'Alice'
pick(target, query, options?)
target
: Target
query
: Query
options
: Options?
Return value: Target
Keep only the properties matching the query
.
const target = {
userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
pick(target, '*./Name/')
// {
// userOne: { firstName: 'Alice', lastName: 'Smith' },
// userTwo: { firstName: 'John', lastName: 'Doe' },
// }
include(target, query, testFunction, options?)
target
: Target
query
: Query
testFunction
: (value) => boolean
options
: Options?
Return value: Target
Keep only the properties that match the query
and that return true
with the
testFunction()
.
const target = {
userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
include(target, '**', (value) => typeof value === 'string')
// {
// userOne: { firstName: 'Alice', lastName: 'Smith' },
// userTwo: { firstName: 'John', lastName: 'Doe' },
// }
exclude(target, query, testFunction, options?)
target
: Target
query
: Query
testFunction
: (value) => boolean
options
: Options?
Return value: Target
Remove any property that matches the query
and that returns true
with the
testFunction()
.
const target = {
userOne: { firstName: 'Alice', lastName: 'Smith', age: 72, admin: true },
userTwo: { firstName: 'John', lastName: 'Doe', age: 72, admin: true },
}
exclude(target, '**', (value) => typeof value === 'string')
// {
// userOne: { age: 72, admin: true },
// userTwo: { age: 72, admin: true },
// }
flatten(target, options?)
target
: Target
options
: Options?
Return value: Target
Flatten deep properties to shallow properties with dot-delimited paths.
const target = { user: { firstName: 'Bob', colors: ['red', 'blue'] } }
flatten(target)
// { 'user.firstName': 'Bob', 'user.colors.0': 'red', 'user.colors.1': 'blue' }
Target
The target value must be an object or an array.
Query
The query format is documented here. Both query strings and arrays can be used.
Options
Options are optional plain objects. They are almost
the same as in wild-wild-path
.
mutate
Methods: map()
,
merge()
,
push()
,
unshift()
,
exclude()
Type: boolean
Default: false
By default, the target is
deeply cloned.
When true
, it is directly mutated instead, which is faster but has side
effects.
const target = { colors: ['red'] }
console.log(push(target, 'colors', ['blue']))
// { colors: ['red', 'blue'] }
console.log(target)
// { colors: ['red'] }
console.log(push(target, 'colors', ['blue'], { mutate: true }))
// { colors: ['red', 'blue'] }
console.log(target)
// { colors: ['red', 'blue'] }
entries
Methods: map()
,
find()
,
include()
,
exclude()
Type: boolean
Default: false
By default, properties' values are:
- Passed as argument to callbacks like
mapFunction()
andtestFunction()
- Returned by
find()
When true
, objects with the following shape are used instead:
value
any
: property's valuepath
Path
: property's full pathmissing
boolean
: whether the property is missing from the target
const target = { job: '', firstName: 'Alice', lastName: 'Smith' }
find(target, '*', (value) => value !== '') // 'Alice'
find(
target,
'*',
(entry) => entry.value !== '' && entry.path[0] !== 'firstName',
{ entries: true },
)
// { value: 'Smith', path: ['lastName'], missing: false },
missing
Methods: map()
,
merge()
,
push()
,
unshift()
Type: boolean
Default: false
with map()
, true
with merge|push|unshift()
When false
, properties
not defined in the target
are ignored.
const target = {}
push(target, 'colors', ['red']) // { colors: ['red'] }
push(target, 'colors', ['red'], { missing: false }) // {}
map(target, 'name', (value = 'defaultName') => value) // {}
map(target, 'name', ({ value = 'defaultName' }) => value, {
missing: true,
entries: true,
}) // { name: 'defaultName' }
sort
Methods: find()
,
pick()
,
include()
,
flatten()
Type: boolean
Default: false
When returning sibling object properties, sort them by the lexigographic order of their names (not values).
const target = { user: { lastName: 'Doe', firstName: 'John', age: 72 } }
flatten(target)
// { 'user.lastName': 'Doe', 'user.firstName': 'John', 'user.age': 72 }
flatten(target, { sort: true })
// { 'user.age': 72, 'user.firstName': 'John', 'user.lastName': 'Doe' }
childFirst
Methods: find()
Type: boolean
Default: false
When using unions or deep wildcards, a query might match both a property and some of its children.
This option decides whether the returned properties should be sorted from children to parents, or the reverse.
const target = { user: { firstName: 'Alice', lastName: '' } }
const isDefined = (value) => value !== ''
find(target, 'user.**', isDefined) // { firstName: 'Alice', lastName: '' }
find(target, 'user.**', isDefined, { childFirst: true }) // 'Alice'
leaves
Methods: map()
,
merge()
,
push()
,
unshift()
,
find()
Type: boolean
Default: false
When using unions or deep wildcards, a query might match both a property and some of its children.
When true
, only leaves are matched. In other words, a matching property is
ignored if one of its children also matches.
const target = { user: { settings: { firstName: 'Alice', lastName: 'Smith' } } }
merge(target, 'user user.settings', { age: 72 })
// {
// user: {
// settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
// age: 72,
// }
// }
merge(target, 'user user.settings', { age: 72 }, { leaves: true })
// {
// user: {
// settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
// }
// }
roots
Methods: map()
,
merge()
,
push()
,
unshift()
,
find()
Type: boolean
Default: false
When using unions or deep wildcards, a query might match both a property and some of its children.
When true
, only roots are matched. In other words, a matching property is
ignored if one of its parents also matches.
const target = { user: { settings: { firstName: 'Alice', lastName: 'Smith' } } }
merge(target, 'user user.settings', { age: 72 })
// {
// user: {
// settings: { firstName: 'Alice', lastName: 'Smith', age: 72 },
// age: 72,
// }
// }
merge(target, 'user user.settings', { age: 72 }, { roots: true })
// {
// user: {
// settings: { firstName: 'Alice', lastName: 'Smith' },
// age: 72,
// }
// }
shallowArrays
Methods: all
Type: boolean
Default: false
If true
, wildcards do
not recurse on arrays. Array items can still be matched by using
indices or
slices.
const target = { user: { firstName: 'Bob', colors: ['red', 'blue'] } }
flatten(target)
// { 'user.firstName': 'Bob', 'user.colors.0': 'red', 'user.colors.1': 'blue' }
flatten(target, { shallowArrays: true })
// { 'user.firstName': 'Bob', 'user.colors': ['red', 'blue'] }
classes
Methods: all
Type: boolean
Default: false
Unless true
, wildcards
and regexps
ignore properties of objects that are not plain objects (like class instances,
errors or functions). Those can still be matched by using their
property name.
const target = { user: new User({ name: 'Alice' }) }
const isDefined = (value) => value !== ''
find(target, 'user.*', isDefined) // undefined
find(target, 'user.*', isDefined, { classes: true }) // 'Alice'
inherited
Methods: all
Type: boolean
Default: false
By default, wildcards and regexps ignore properties that are either inherited or not enumerable. Those can still be matched by using their property name.
When true
, inherited properties are not ignored, but not enumerable ones still
are.
Related projects
wild-wild-path
: object property paths used bywild-wild-utils
wild-wild-parser
: parser forwild-wild-path
's object property pathsdeclarative-merge
: object merging logic used by themerge()
methodset-array
: array update logic used by themerge()
method
Support
For any question, don't hesitate to submit an issue on GitHub.
Everyone is welcome regardless of personal background. We enforce a Code of conduct in order to promote a positive and inclusive environment.
Contributing
This project was made with β€οΈ. The simplest way to give back is by starring and sharing it online.
If the documentation is unclear or has a typo, please click on the page's Edit
button (pencil icon) and suggest a correction.
If you would like to help us fix a bug or add a new feature, please check our guidelines. Pull requests are welcome!
<!-- Thanks go to our wonderful contributors: --> <!-- ALL-CONTRIBUTORS-LIST:START --> <!-- prettier-ignore --> <!-- <table><tr><td align="center"><a href="https://fosstodon.org/@ehmicky"><img src="https://avatars2.githubusercontent.com/u/8136211?v=4" width="100px;" alt="ehmicky"/><br /><sub><b>ehmicky</b></sub></a><br /><a href="https://github.com/ehmicky/wild-wild-utils/commits?author=ehmicky" title="Code">π»</a> <a href="#design-ehmicky" title="Design">π¨</a> <a href="#ideas-ehmicky" title="Ideas, Planning, & Feedback">π€</a> <a href="https://github.com/ehmicky/wild-wild-utils/commits?author=ehmicky" title="Documentation">π</a></td></tr></table> --> <!-- ALL-CONTRIBUTORS-LIST:END -->