import { isArray } from "@/helpers/is-array.ts";
import { isFunction } from "@/helpers/is-function.ts";
import { isIterable } from "@/helpers/is-iterable.ts";
import { isPlainObject } from "@/helpers/is-plain-object.ts";
import { last } from "@/helpers/last.ts";
export interface TraverseOptions<Key = string | number | symbol> {
* A function that returns the keys of the object to be traversed.
ownKeys?: ((parent: object) => Iterable<Key>) | null;
* When true, the visitor callback will be invoked for the root object.
rootNeedsVisit?: boolean | null;
* Recursively visit each property of an object (or each element of an
* array) and its nested objects or arrays. By default, the only
* nested objects to be traversed are plain objects and arrays.
* const root = { a: 1, b: { c: { d: [2] }, e: 3 } }
* traverse(root, (value, key, parent, context) => {
* console.log(key, '=>', value)
export function traverse(
visitor: TraverseVisitor,
options?: (TraverseOptions & { rootNeedsVisit?: null }) | null,
outerContext?: TraverseContext,
export function traverse(
visitor: TraverseVisitor<keyof any | null>,
options?: TraverseOptions<keyof any | null> | null,
outerContext?: TraverseContext<keyof any | null>,
export function traverse(
visitor: TraverseVisitor<any>,
options?: TraverseOptions<any> | null,
outerContext?: TraverseContext<any> | null,
const context = (outerContext ?? {
context.skipped.add(obj ?? context.value);
}) as TraverseContext<keyof any | null> & {
const { rootNeedsVisit } = (options ??= {});
const ownKeys = options.ownKeys ?? Object.keys;
// This is called for each value in an object or iterable.
const visit = (value: unknown, key: keyof any): boolean => {
// Map entries are destructured into value and key.
if (context.parent.constructor === Map) {
[key, value] = value as [keyof any, unknown];
// Traverse nested plain objects and arrays that haven't been
// skipped and aren't a circular reference.
typeof value === "object" &&
(isArray(value) || isPlainObject(value)) &&
!context.skipped.has(value) &&
!context.parents.includes(value)
parentResult?: ReturnType<TraverseVisitor>,
context.parents.push(parent);
if (rootNeedsVisit && parent === root) {
(context.value = parent),
if (parentResult === false) {
// Use Array.prototype.forEach for arrays so that sparse arrays
// are efficiently traversed. The array must be cloned so it can
// be cleared if the visitor returns false.
parent.slice().forEach((value, index, values) => {
if (visit(value, index) === false) {
values.length = 0; // Stop further traversal.
// Allow an iterable (e.g. a Map or a Set) to be passed in as the
else if (parent === root && isIterable(parent)) {
for (const value of parent) {
if (visit(value, index) === false) {
// Traverse the object's properties.
for (const key of ownKeys(parent)) {
if (visit(parent[key as keyof object], key) === false) {
context.parent = last(context.parents);
// Invoke the leave callback for the completed parent.
if (ok && isFunction(parentResult)) {
ok = parentResult() !== false;
// If this is a recursive call, it's possible that the root object
// was skipped earlier. Check for that here so the caller doesn't
if (outerContext.skipped.has(root)) {
// We'll have to restore the context after the traversal because
// it might be used by the caller.
const { value, key } = context;
* The visitor callback for the `traverse` function.
export type TraverseVisitor<Key = keyof any> = (
context: TraverseContext<Key>,
options: TraverseOptions<Key> & { rootNeedsVisit?: null },
) => (() => boolean | void) | boolean | void;
* The context object for the `traverse` function.
export interface TraverseContext<Key = keyof any> {
* The current value being traversed.
* The property key or index with which the current value was found.
* The parent object of the current value.
* The stack of parent objects. The last object is the current
* ⚠️ This array must not be mutated directly or referenced outside
* the `visitor` callback. If that's necessary, you'll want to clone
readonly parents: readonly object[];
* The path to the `parent` object. The last key is the current key.
* ⚠️ This array must not be mutated directly or referenced outside
* the `visitor` callback. If that's necessary, you'll want to clone
readonly path: readonly (keyof any)[];
* When the current value is a traversable object/iterable, this
* function can be used to skip traversing it altogether.
* If the `obj` argument is provided, it marks the given object as
* skipped (instead of the current value), preventing it from being
readonly skip: (obj?: object) => void;
readonly skipped: ReadonlySet<unknown>;