Handling Complex Recursive Data Structures Safely in TypeScript

Learn how to define and manage complex recursive data structures in TypeScript with safety and ease, using interfaces, types, and runtime checks.

Recursive data structures are widely used in programming to represent hierarchical or nested data, such as trees, nested menus, or file directories. In TypeScript, handling these structures safely can be tricky for beginners because the types need to refer to themselves in some way. This tutorial will walk you through how to define recursive types in TypeScript and how to work with them safely.

Let's imagine we want to represent a simple file system where folders can contain files or other folders. This requires a recursive data structure because folders can nest infinitely.

First, we'll define two types: one for a file and one for a folder. The folder will contain an array of items, each of which can be either a file or another folder.

typescript
interface File {
  type: "file";
  name: string;
  size: number; // size in bytes
}

interface Folder {
  type: "folder";
  name: string;
  children: FileSystemItem[]; // recursive reference here
}

type FileSystemItem = File | Folder;

Here, the `FileSystemItem` type is a union type that can be either a `File` or a `Folder`. The `Folder` contains a `children` array with the same union type, making it recursive. This setup allows us to create nested folders and files.

Now let's create an example data structure using this type:

typescript
const myFileSystem: FileSystemItem = {
  type: "folder",
  name: "root",
  children: [
    {
      type: "file",
      name: "photo.png",
      size: 3400
    },
    {
      type: "folder",
      name: "documents",
      children: [
        {
          type: "file",
          name: "resume.pdf",
          size: 12000
        }
      ]
    }
  ]
};

To work with this data safely, we often need to traverse it. When traversing, it’s important to check the type of each item to correctly handle files and folders.

Here’s a simple example function that calculates the total size of all files inside a folder (including nested folders):

typescript
function getTotalSize(item: FileSystemItem): number {
  if (item.type === "file") {
    return item.size;
  } else {
    return item.children.reduce((total, child) => total + getTotalSize(child), 0);
  }
}

console.log(`Total size: ${getTotalSize(myFileSystem)} bytes`);

This function works recursively by checking the item type. If it's a file, it returns its size. If it's a folder, it sums the sizes from all its children by recursively calling itself.

For better safety especially when consuming external or dynamic data, consider runtime type checks or validation libraries like `io-ts` or `zod`. These tools can help ensure your data matches the expected recursive structure before processing it.

In summary, handling complex recursive data structures in TypeScript involves: - Defining recursive types using interfaces and union types. - Using type guards to distinguish among types during runtime. - Recursively processing nested data safely. With these foundations, you can confidently manage deeply nested data in your TypeScript projects.