ENTRY LOADED

Stories // log entry

Create a tree library with functional typescript

January 3, 2023
data structure and algorithmstypescript

Most of the time in my college days, I had been learning about the binary tree, where each node has no more than two children. In my work, I more often see a tree with tons of children in a single node, though. In this article, I am going to use Typescript.

About the tree, and about Typescript

Most of the time in my college days, I had been learning about the binary tree, where each node has no more than two children. In my work, I more often see a tree with several children in a single node, though.

In this article, I am going to use Typescript. These days, I mostly work with the .NET (using C#) and Angular frameworks (using Typescript), but I have solved a similar problem in Angular, so, yes, let’s do it again and try to improve it this time.

Define a type

Pretend that I had created a.ts file called tree.ts. First of all, let’s define an interface called INode that takes a type argument indicating its id type. Every node in the tree should have its own id and a child list.

typescript
export interface INode<T extends string | number> { id: T; children: INode<T>[]; }

Iterate nodes

The first thing that comes to mind is probably traversing through nodes. Fortunately, Typescript (actually Javascript) has the array’s foreach built-in function, so we just leverage that.

typescript
export function foreachNode<T extends string | number>( nodes: INode<T>[] | INode<T>, callbackFn: (node: INode<T>) => void ): void { const foreachFn = (node: INode<T>) => { callbackFn(node); node.children.forEach(callbackFn); }; Array.isArray(nodes) ? nodes.forEach(foreachNode) : foreachFn(nodes); }

My plan is to invoke the callback argument on a specific node, then recursively execute the same callback on each element of its child list. After all, it looks like the code above.

Find a node (DFS)

So far, we have our way of traversing the tree in depth. I am going to use the same approach to do a linear search on each child list.

If the tree's depth is greater than its width, this method will outperform BFS in terms of performance. The implementation should be like this

typescript
export function findNode<T extends string | number>( nodes: INode<T>[] | INode<T>, predicate: (node: INode<T>) => boolean ): INode<T> | undefined { const findFn = (node: INode<T>) => predicate(node) ? node : find(node.children, predicate); return !Array.isArray(nodes) ? findFn(nodes) : nodes.find(node => findFn(node)); }

Convert a flat list to tree

Define the flat list

typescript
export interface IFlatModel<T extends string | number> { id: T; parentId?: T; } export interface INode<T extends string | number> { id: T; children: INode<T>[]; } export type IList<T extends string | number> = IFlatModel<T>[];

Flat list to tree

typescript
export function toTree<T extends string | number>(flatList: IList<T>): Array<INode<T>> { return convert(flatList); }
typescript
export function foreach<T extends string | number>( nodes: INode<T>[] | INode<T>, callbackFn: (node: INode<T>) => void ): void { const foreachNode = (node: INode<T>) => { callbackFn(node); node.children.forEach(callbackFn); }; Array.isArray(nodes) ? nodes.forEach(foreachNode) : foreachNode(nodes); } export function find<T extends string | number>( nodes: INode<T>[] | INode<T>, predicate: (node: INode<T>) => boolean ): INode<T> | undefined { const findFn = (node: INode<T>) => predicate(node) ? node : find(node.children, predicate); return !Array.isArray(nodes) ? findFn(nodes) : nodes.find(node => findFn(node)); } function convert<T extends string | number>( list: IList<T> | INode<T>[] ): INode<T>[] { const map = new Map<T, INode<T>>(); // create nodes without children for (const item of list) { const node = { ...item, children: [] }; map.set(node.id, node); } // link nodes with their parents for (const node of map.values()) { if (node.parentId !== undefined) { const parent = map.get(node.parentId); if (parent !== undefined) { parent.children.push(node); } else { console.warn(`Cannot find parent node for ${node.id}`); } } } // find root nodes const roots: INode<T>[] = []; for (const node of map.values()) { if (node.parentId === undefined) { roots.push(node); } } return roots; }

Demo

🔗 You can see the code here on Github gits

💡 And the demo here on Stackblitz