Skip to content

Instantly share code, notes, and snippets.

@tohagan
Last active February 29, 2024 06:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tohagan/15f7cb8c1a90f4f2f94925d154163f2e to your computer and use it in GitHub Desktop.
Save tohagan/15f7cb8c1a90f4f2f94925d154163f2e to your computer and use it in GitHub Desktop.
Typescript function to visit all nodes in a JS object. Visitor pattern
export type TraversePath = Array<string|number> | undefined;
export type TraverseVisitor = (parent: any, key: string | number, val: any, path: TraversePath) => void;
export function traverse(obj: any, visit: TraverseVisitor, path: TraversePath) {
function perNode(key: string | number, val: any) {
const path1 = path ? path.concat([key]) : undefined;
visit(obj, key, val, path1);
traverse(val, visit, path1);
}
if (typeof obj === "object") {
for (const key in Object.keys(obj)) perNode(key, obj[key]);
} else if (Array.isArray(obj)) {
for (let key = 0; key < obj.length; key++) perNode(key, obj[key]);
}
}
export type Visitor = (
parentPath: string,
childKey: string,
parent: VisitDocument,
child: unknown
) => boolean
export type VisitDocument = Record<string | number, unknown>
export function isVisitDocument(obj: any): obj is VisitDocument {
return typeof obj === "object"
}
/**
* Recusively visits all keys in a nested object or array.
* @param parent Object or Array being visited.
* @param visit Visit function called once per next object key. Recurse tree when true returned
* @param path Initial path.
*
* obj can be Object | Array because `Object.keys()` and `obj[key]` works for both types.
*/
export function visit(
parent: VisitDocument,
visitor: Visitor,
path = ''
) {
// Use .sort() if a predictable visit order is required
const keys = Object.keys(parent) //.sort()
for (const key of keys) {
const child = parent[key]
if (visitor(path, key, parent, child)) {
if (isVisitDocument(child)) { // object or array
// visitor(val, visit, path.concat([key])); // array path
visit(child, visitor, `${path}/${key}`) // string path
}
}
}
}
@tohagan
Copy link
Author

tohagan commented Apr 5, 2019

If you need to get a path from each visit() callback, send [] as initial path.
Does not call visit() on the root node.

@tohagan
Copy link
Author

tohagan commented May 31, 2019

Since JS treats Arrays and Objects as the same thing you can actually fold the 2 cases.
This was my final solution.

export type VisitorPath = Array<string>;
export type VisitorVisit = (path: VisitorPath, obj: any, key: string, val: any) => boolean;

// obj can be Object | Array because Object.keys() works for both types.
export function visitor(obj: { [id: string]: any }, visit: VisitorVisit, path: VisitorPath = []) {
  // eslint-disable-next-line guard-for-in
  for (const key in obj) {
    const val = obj[key];
    if (visit(path, obj, key, val)) {
      if (typeof val === "object") {
        visitor(val, visit, path.concat([key]));
      }
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment