Skip to content

Differences from traverse

traverse is the classic this-bound walker. neotraverse keeps that API as a drop-in (now at neotraverse/legacy), ships a utility-first functional API as the default neotraverse export, and hardens the engine for real-world JSON and config trees.

Pick your path

Stay on traverse syntax? import traverse from 'neotraverse/legacy', one-line swap.

Starting fresh or want tree-shaking? import { forEach } from 'neotraverse'.

Step-by-step upgrade? See Migrating from traverse.

At a glance

traverseneotraverse
DependenciesShips with runtime depsZero, no polyfills
Types@types/traverseBuilt in
Untrusted JSONClassic behaviourHardened, pollution & injection safe (Security)
ThroughputBaseline~3× drop-in (neotraverse/legacy) · ~5× functional API (neotraverse, up to ~10× on core walks, Benchmarks)
Memory / walkBaseline~2× less allocation (drop-in) · ~6× less (functional, up to ~11× on wide forEach, Benchmarks)
Bundle (functional)Monolithic importTree-shakeable (sideEffects: false), ~2 to 6 KB brotli for typical apps
Default APItraverse(obj).forEach(fn)Functional on neotraverse; classic drop-in on neotraverse/legacy
Recommended new code-neotraverse, forEach(obj, (ctx, x) => …)

What stays the same

On the legacy build, the mental model is unchanged:

  • traverse(obj) returns an instance with .forEach, .map, .reduce, .paths, .nodes, .clone, .get, .set, .has
  • Callbacks use this as the traversal context (this.update, this.path, this.isLeaf, …)
  • Options and return shapes match what you already know from traverse
ts
import traverse from 'neotraverse/legacy';

traverse({ a: 1, b: 2 }).forEach(function (x) {
  if (typeof x === 'number') this.update(x * 10);
});

What changes (even as a drop-in)

You get these wins without rewriting callbacks:

ChangeWhy it matters
No @types/traverseTypes ship with the package
Prototype-pollution safetyclone / map / set refuse hostile __proto__ keys (details)
Faster, leaner walksSame call shape, higher ops/sec and ~2× less heap per op (benchmarks)
Drop-in lives at /legacyimport traverse from 'neotraverse/legacy' (ES2015, CJS + ESM); the classic this-bound API stays behaviour-compatible with traverse

The functional API (default neotraverse export)

This is the biggest optional difference, not required to migrate, but what most new projects should use. It is the recommended entry point.

Callback shape: thisctx

traverse / neotraverse/legacyneotraverse (functional)
Stylefunction (x) { this.update(…) }(ctx, x) => { ctx.update(…) }
ContextthisFirst argument ctx
ImportsDefault export, chained methodsNamed functions, tree-shakeable
ts
import { forEach } from 'neotraverse';

forEach({ a: 1, b: 2 }, (ctx, x) => {
  if (typeof x === 'number') ctx.update(x * 10);
});

Every this.* field has the same name on ctx (Context reference).

Options move to the last argument

ts
// traverse
traverse(obj, { immutable: true }).map(fn);

// functional
import { map } from 'neotraverse';
map(obj, fn, { immutable: true });

Class API is deprecated

new Traverse(obj) still works in 1.0 but is removed in v2. It lives at neotraverse/modern and is trimmed to the legacy method set only. Prefer the named functions from the default neotraverse export.

Only on the functional API (no traverse equivalent)

These are additive, your old code keeps working; you opt in when you need them.

AreaExamples
String pathsgetPath, setPath, hasPath, parseDotPath, parseJsonPointer
Path searchfindPaths, filterPaths, select (glob), count, size
StructuraldeleteWhere, prune, pruneDeep, deepEqual, toJSON, freeze, diff, patch
Walk controlwalk, breadthFirst, mapBfs, skipWhere, groupBy, merge, dereference
TypinggetType, explicit node kinds (object, array, Map, Set, …)
Lazy iterationentries, values generators
AsyncforEachAsync, mapAsync with concurrency and AbortSignal
Map / SetdescendIntoMapSet, traverse collection entries, not just object keys

Full reference: API docs · Example index.

Three ways to adopt

sh
npm install neotraverse
npm uninstall traverse @types/traverse
diff
-import traverse from 'traverse';
+import traverse from 'neotraverse/legacy';
js
// vite.config.js
export default {
  resolve: { alias: { traverse: 'neotraverse/legacy' } }
};

Then optionally move hot paths to the functional neotraverse API for tree-shaking and arrow-friendly ctx callbacks.

Which build should I use?

BuildImportUse when
Default (functional)neotraverseNew apps, TypeScript, tree-shaking, extra helpers, fastest get/set; the recommended entry
Deprecated classneotraverse/modernOnly for existing Traverse class users; deprecated, removed in v2
Legacyneotraverse/legacyDrop-in replacement; this-bound API; older bundlers / CommonJS; still traverse-compatible (ES2015)

What you keep from traverse

  • Deep walks, in-place updates, immutable map, reduce, path helpers on the instance
  • The same context vocabulary (update, delete, remove, block, skip, keys, parents, circular handling)
  • Familiar ergonomics on the legacy build, Legacy / Classic API

Next steps

Released under the MIT License.

157.47Mtotal npm downloadssince Dec 2024