Last active
February 29, 2024 06:09
-
-
Save tohagan/15f7cb8c1a90f4f2f94925d154163f2e to your computer and use it in GitHub Desktop.
Typescript function to visit all nodes in a JS object. Visitor pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} | |
} |
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
If you need to get a path from each
visit()
callback, send[]
as initial path.Does not call
visit()
on the root node.