Open In App

What is Recursive Generic in TypeScript ?

In TypeScript, a recursive generic is a type that refers to itself within its definition, enabling the creation of data structures or types containing references to the same type within their structure. This capability proves invaluable when dealing with nested or hierarchical data structures like trees, linked lists or graphs. In this article, we’ll explore the syntax, various approaches, and examples of using recursive generics in TypeScript, with additional insights into their practical applications.

Using interfaces

Interfaces provide a way to define the structure of objects in TypeScript without implementing the details. They are useful for defining recursive generic types, particularly when describing the shape of a type.

Syntax:

interface ListNode<T> {
value: T;
next: ListNode<T> | null;
}

T: It is the type parameter that represents the type of the node value and the type of the next node. One can then use this type to create a linked list of any type, such as ListNode<number> or ListNode<string>.



Example: In the example below, ListNode<T> represents a node in a linked list, where T is the type of the node’s value and the type of the next node.




interface ListNode<T> {
  value: T;
  next: ListNode<T> | null;
}
 
const list: ListNode<number> = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3,
      next: null
    }
  }
};
 
console.log(list);

Output:

{ value: 1, next: { value: 2, next: { value: 3, next: null } } }

Example: A function that calculates the depth of a tree.




interface Tree<T> {
  value: T;
  children: Array<Tree<T>>;
}
 
function depth<T>(tree: Tree<T>): number {
   
  if (tree.children.length === 0) {
    return 1;
  }
   
  return Math.max(...tree.children.map(child => depth(child))) + 1;
}
 
 
const tree: Tree<number> = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        {
          value: 3,
          children: []
        },
        {
          value: 4,
          children: []
        }
      ]
    },
    {
      value: 5,
      children: [
        {
          value: 6,
          children: []
        }
      ]
    }
  ]
};
 
 
console.log(depth(tree));

Output:

3

Using type aliases

Type aliases provide a way to give a type a name in TypeScript. They are another approach for defining recursive generic types, particularly useful when you want to create a new name for a type.

Syntax:

type Tree<T> = {
value: T;
children: Array<Tree<T>>;
}

Tree<T>: represents a tree node with a value of type T and an array of children nodes of the same type.

Example: In the example below, Tree<T> represents a tree node with a value of type T and an array of children nodes of the same type.




type Tree<T> = {
  value: T;
  children: Array<Tree<T>>;
}
 
const tree: Tree<number> = {
  value: 1,
  children: [
    { value: 2, children: [{ value: 3, children: [] }] },
    { value: 4, children: [{ value: 5, children: [] }] }
  ]
};
 
console.log(tree);

Output

{ value: 1, children: [ { value: 2, children: [ { value: 3, children: [] } ] }, { value: 4, children: [ { value: 5, children: [] } ] } ] }

Example: A function that reverses a linked list




type ListNode<T> = {
  value: T;
  next: ListNode<T> | null;
}
 
 
function reverse<T>(head: ListNode<T>): ListNode<T> {
   
  if (head === null || head.next === null) {
    return head;
  }
   
  const newHead = reverse(head.next);
  head.next.next = head;
  head.next = null;
  return newHead;
}
 
// Create a sample linked list
const list: ListNode<string> = {
  value: "a",
  next: {
    value: "b",
    next: {
      value: "c",
      next: null
    }
  }
};
 
 
console.log(reverse(list));

Output:

{   "value": "c",   "next": {     "value": "b",     "next": {       "value": "a",       "next": null     }   } } 

Using classes

Classes in TypeScript allow you to create objects with properties and methods. They are another approach for defining recursive generic types, suitable when you want to define the behavior and methods of a type along with its structure.

Syntax:

class BinaryTreeNode<T> {
value: T;
left: BinaryTreeNode<T> | null;
right: BinaryTreeNode<T> | null;
constructor(value: T, left: BinaryTreeNode<T> | null = null, right: BinaryTreeNode<T> | null = null) {
this.value = value;
this.left = left;
this.right = right;
}
}

Here, BinaryTreeNode<T> represents a binary tree node with a value of type T and two child nodes of the same type.

Example: In the example below, BinaryTreeNode<T> represents a binary tree node with a value of type T and two child nodes of the same type.




class BinaryTreeNode<T> {
  value: T;
  left: BinaryTreeNode<T> | null;
  right: BinaryTreeNode<T> | null;
 
  constructor(value: T, left: BinaryTreeNode<T> | null = null, right: BinaryTreeNode<T> | null = null) {
    this.value = value;
    this.left = left;
    this.right = right;
  }
}
 
const binaryTree: BinaryTreeNode<string> = new BinaryTreeNode(
  "root",
  new BinaryTreeNode("left"),
  new BinaryTreeNode("right")
);
 
console.log(binaryTree);

Output :

BinaryTreeNode { value: 'root', left: BinaryTreeNode { value: 'left', left: null, right: null }, right: BinaryTreeNode { value: 'right', left: null, right: null } }

Article Tags :