# wild-wild-path and the query language for object graphs

> wild-wild-path is a JavaScript library that reads and writes nested object properties using glob-like paths, wildcards, regular expressions, slices and unions. It is aimed at code that needs to select properties across a shape it does not know in advance, and it trades a dependency-light, functional API for an ES module only build.

**ehmicky/wild-wild-path** — 🤠 Object property paths with wildcards and regexps 🌵

- Repository: https://github.com/ehmicky/wild-wild-path
- Stars: 728 · Forks: 15
- Language: JavaScript
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/ehmicky-wild-wild-path

## The library treats a nested object as something you can glob

wild-wild-path is for reading and writing object properties through a path syntax. The README leads with five ways of expressing a query: dot-delimited paths such as `foo.bar.0.baz`, wildcards such as `foo.*` and a double star that matches any depth, regular expressions such as `foo./ba?/`, array slices such as `foo.0:2`, and unions such as `foo bar baz`.

That list is the concept. A filesystem path addresses one location, so `foo.bar.0.baz` is the only shape it can express. A glob addresses a set of locations, and wild-wild-path is a glob engine for JavaScript objects.

The use case follows from that. When you know the shape of your data, `obj.users[3].email` is enough and a library is overkill. When the shape varies, when you want every `firstName` at any depth, or every element of an array, or every key matching a pattern, you need something that walks the graph. That is the niche this fills, and the deep double star wildcard is the feature that makes it more than a convenience wrapper around property access.

## Five query forms, and the regex form comes in two spellings

The query language has five pieces and they compose. Dot notation addresses a literal key or an array index. A single star wildcard matches one level, and a double star matches any number of levels. A regexp in slash form is written inline in the string, as `foo./ba?/`, while the same pattern can also be given as a RegExp object inside an array query. A slice such as `foo.0:2` selects a range of array indices, and a union written with spaces selects several paths at once.

A query can also be given as an array of path segments, `['settings', 'colors', 0]`, which is the form to use when a key contains characters that would otherwise be parsed as syntax.

The two spellings of a regexp are worth noting because they behave differently in one respect that the README shows. Given a target with `userOne` holding `firstName` and `lastName`, the string form `userOne./Name/` and the array form `['userOne', /Name/]` both return the two matching values. The array form is the one to reach for when the pattern itself contains a dot or a slash, since those characters would otherwise be read as path syntax.

## get returns one match, list returns every match

The method set is small and the difference between the accessors is the first thing to understand.

`get(target, query, options?)` returns the first property matching the query, with a return type of `any | undefined`. With a plain path it is property access with an undefined for a miss:

```js
const target = { settings: { colors: ['red', 'blue'] } }

get(target, 'settings.colors.0') // 'red'
get(target, ['settings', 'colors', 0]) // 'red'
```

`has(target, query, options?)` answers whether the query matches any property, and it distinguishes a key that exists with an undefined value from one that is absent. That distinction shows up in the README's example, where `settings.firstName` is `false` while `settings.lastName` is `true` even though the latter is `undefined`.

`list(target, query, options?)` returns every match as an array, which is where wildcards, deep wildcards, regexps and slices pay off:

```js
list(target, 'userOne./Name/') // ['John', 'Doe']
list(target, ['userOne', /Name/]) // ['John', 'Doe']

list(target, 'userTwo.colors.*') // ['red', 'blue', 'yellow']
list(target, 'userTwo.colors.0:2') // ['red', 'blue']
```

Because `get` returns the first match and `list` returns all of them, a wildcard query through `get` will silently give you one arbitrary result rather than an error. Use `list` when the query is not a literal path.

## set and remove clone the target unless you ask for mutation

`set(target, query, value, options?)` sets all properties matching the query, and `remove(target, query, options?)` deletes all of them. Both return a `Target`, and the README is explicit that the return value is a deep clone unless the `mutate` option is `true`.

That default is the library's most consequential design choice. A functional, copy-on-write API is pleasant to reason about and composes with immutable state patterns, but it costs a full deep copy of the target on every write. For a large object in a hot loop, that is the difference between acceptable and not.

The semantics of index paths are also more capable than they first look:

```js
set(target, 'colors.0', 'yellow') // ['yellow', 'blue']
set(target, ['colors', 0], 'yellow') // ['yellow', 'blue']
set(target, 'colors.-1', 'yellow') // ['red', 'yellow']
set(target, 'colors.-0', 'yellow') // ['red', 'blue', 'yellow']
set(target, 'colors.*', 'yellow') // ['yellow', 'yellow']
set({}, 'user.0.color', 'red') // { user: [{ color: 'red' }] }
set({}, 'user.0.color', 'red', { missing: false }) // {}
```

Negative indices count from the end, so `colors.-1` replaces the last element while `colors.-0` appends. Setting `colors.*` overwrites every element. And writing through a path that does not exist creates the intermediate structure, unless the `missing` option is set to `false`, in which case the write is skipped and the target comes back unchanged.

## entries: true returns the path as an array alongside each value

One option deserves its own mention because it changes what you can do with the results. With `entries: true`, `list()` returns objects rather than bare values, each carrying the value, the path and a `missing` flag:

```js
list(target, 'userOne.*', { entries: true })
// [
//   { value: 'John', path: ['userOne', 'firstName'], missing: false },
//   { value: 'Doe', path: ['userOne', 'lastName'], missing: false },
//   { value: 72, path: ['userOne', 'age'], missing: false },
// ]
```

The path comes back as an array of segments rather than a string, which means you can feed it straight back into `get`, `set` or `remove` as a query. That round trip is the difference between a library you can use to explore an unknown structure and one you can only use to fetch known keys.

Note that the `missing` flag exists even when it is false in this example, which suggests the option reports properties that the query matched but that could not be resolved to a value. The README does not expand on that case, so treat it as a signal rather than something to rely on.

## iterate is the memory conscious variant of list

`iterate(target, query, options?)` returns an iterable rather than an array. The README describes the trade-off directly: it is slower than `list()` but uses less memory.

That is a straightforward engineering decision. `list()` has to materialise every match before returning, so a query such as a deep wildcard over a large object holds all of the results in memory at once. The iterable yields them one at a time, so peak memory tracks the largest single match rather than the whole result set.

```js
const target = { settings: { colors: ['red', 'blue'] } }

for (const color of iterate(target, 'settings.colors.*')) {
  console.log(color) // 'red', 'blue'
}
```

The pattern to reach for is a deep wildcard or a broad regexp over a big object, where the number of matches is unknown in advance. For a handful of matches, `list()` is simpler and the difference does not matter.

Removing properties follows the same path syntax, including the regexp forms:

```js
const target = { user: { firstName: 'John', lastName: 'Doe', age: 72 } }

remove(target, 'user.lastName') // { user: { firstName: 'John', age: 72 } }
remove(target, 'user./Name/') // { user: { age: 72 } }
remove(target, ['user', /Name/]) // { user: { age: 72 } }
```

## ES module only, two runtime dependencies, and a separate utilities package

Installation is a single npm command:

```bash
npm install wild-wild-path
```

The package works in Node.js 18.18.0 and above and in browsers, and it is an ES module. The README is firm about the consequence: it must be loaded with an `import` or `import()` statement, not `require()`, and a TypeScript project must be configured to output ES modules rather than CommonJS. `package.json` confirms the constraint with `"type": "module"` and an `engines` field of `"node": ">=18.18.0"`.

Runtime dependencies are only two:

```json
  "dependencies": {
    "is-plain-obj": "^4.1.0",
    "wild-wild-parser": "^5.0.1"
  },
```

The parser is a separate package, which means the query syntax is implemented and versioned independently of the traversal logic. The plain object check is what keeps the library from walking into class instances and other exotic objects.

Higher level operations live in a different library entirely, `wild-wild-utils`, which provides `map()`, `merge()`, `push()`, `unshift()`, `find()`, `pick()`, `include()`, `exclude()` and `flatten()` on top of the same query language. If you need those, you install a second package.

On versions and licence, the releases are v5.0.2 from 2026-08-23, v5.0.1 from 2025-03-29 and v5.0.0 from 2023-10-28, the last push was on 2026-10-13, and the licence is Apache-2.0.

## Conclusion

wild-wild-path suits code that manipulates configuration objects, API responses and settings trees whose exact shape varies at runtime, especially where you need to select every match rather than a single known key. It is the wrong choice for a CommonJS project, since the package is ES module only and must be loaded with `import` or `import()`, and for hot paths where the default deep clone on `set()` and `remove()` is too costly. Verify first that your build tooling outputs ES modules, and check the `entries: true` option of `list()` against the path format your own code expects, since it returns the path as an array rather than a string.

## FAQ

### How do I install wild-wild-path?

The README gives `npm install wild-wild-path`. The package is an ES module, so it must be loaded with an `import` or `import()` statement rather than `require()`, and it requires Node.js 18.18.0 or above.

### How do I match every property at any depth with wild-wild-path?

A double star wildcard in the query returns every matching value at any depth of the object. A single star such as `userTwo.colors.*` matches one level only.

### Can wild-wild-path match property names with a regular expression?

Yes. A regexp can be written inline in the query string, for example `foo./ba?/`, or given as a RegExp object in an array query such as `['userOne', /Name/]`. Both forms return every matching value when used with `list()`.

### Does wild-wild-path modify the object I pass in?

By default it does not. `set()` and `remove()` return a deep clone of the target, and the README states that the return value is a deep clone unless the `mutate` option is `true`. The `entries: true` option of `list()` returns the path of each match as an array of segments.

### What licence is wild-wild-path released under?

The package is published under the Apache-2.0 licence, declared in `package.json` and with the text in the LICENSE file at the repository root.

## Sources

- [ehmicky/wild-wild-path on GitHub](https://github.com/ehmicky/wild-wild-path)
- [Issues](https://github.com/ehmicky/wild-wild-path/issues)
- [License: Apache-2.0](https://github.com/ehmicky/wild-wild-path/blob/main/LICENSE)
- [README](https://github.com/ehmicky/wild-wild-path/blob/main/README.md)
- [Releases](https://github.com/ehmicky/wild-wild-path/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ehmicky-wild-wild-path
