Перейти к содержимому
28 / 36

Как типизировать рекурсивную структуру данных (дерево)?


Полный ответ

Базовое дерево:

TypeScript
// Узел дерева — children ссылается на тот же тип
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

const tree: TreeNode<string> = {
  value: "root",
  children: [
    {
      value: "child-1",
      children: [
        { value: "grandchild-1", children: [] },
      ],
    },
    {
      value: "child-2",
      children: [],
    },
  ],
};

Бинарное дерево:

TypeScript
// Бинарное дерево — left/right или null
interface BinaryTree<T> {
  value: T;
  left: BinaryTree<T> | null;
  right: BinaryTree<T> | null;
}

const bst: BinaryTree<number> = {
  value: 10,
  left: {
    value: 5,
    left: { value: 2, left: null, right: null },
    right: { value: 7, left: null, right: null },
  },
  right: {
    value: 15,
    left: null,
    right: { value: 20, left: null, right: null },
  },
};

Рекурсивные операции с правильными типами:

TypeScript
// Поиск в глубину — TypeScript отслеживает тип T

function find<T>(node: TreeNode<T>, predicate: (value: T) => boolean): T | undefined {
  if (predicate(node.value)) return node.value;

  for (const child of node.children) {
    const result = find(child, predicate);
    if (result !== undefined) return result;
  }

  return undefined;
}

// map по дереву — преобразование значений

function mapTree<T, U>(node: TreeNode<T>, fn: (value: T) => U): TreeNode<U> {
  return {
    value: fn(node.value),
    children: node.children.map((child) => mapTree(child, fn)),
  };
}

const numTree = mapTree(tree, (s) => s.length);

// TreeNode<number> — TypeScript вывел тип

Вложенное меню с discriminated union:

TypeScript
// Меню: элемент либо ссылка, либо группа с подменю
type MenuItem =
| { kind: "link"; label: string; href: string }
| { kind: "group"; label: string; children: MenuItem[] };

const menu: MenuItem[] = [
  { kind: "link", label: "Home", href: "/" },
  {
    kind: "group",
    label: "Products",
    children: [
      { kind: "link", label: "Laptop", href: "/laptop" },
      {
        kind: "group",
        label: "Accessories",
        children: [
          { kind: "link", label: "Mouse", href: "/mouse" },
        ],
      },
    ],
  },
];

function renderMenu(items: MenuItem[], depth = 0): string {
  return items
  .map((item) => {
      const indent = "  ".repeat(depth);
      if (item.kind === "link") {
        return `${indent}<a href="${item.href}">${item.label}</a>`;
      }
      return `${indent}<div>${item.label}</div>
      ${renderMenu(item.children, depth + 1)}`;
    })
  .join("
    ");
}

Файловая система:

TypeScript
type FSEntry =
| { type: "file"; name: string; size: number }
| { type: "dir"; name: string; entries: FSEntry[] };

function totalSize(entry: FSEntry): number {
  if (entry.type === "file") return entry.size;
  return entry.entries.reduce((sum, e) => sum + totalSize(e), 0);
}

const project: FSEntry = {
  type: "dir",
  name: "src",
  entries: [
    { type: "file", name: "index.ts", size: 1024 },
    {
      type: "dir",
      name: "utils",
      entries: [
        { type: "file", name: "helpers.ts", size: 512 },
      ],
    },
  ],
};

console.log(totalSize(project)); // 1536

Реальные кейсы

AST (абстрактное синтаксическое дерево):

TypeScript
type Expression =
| { type: "number"; value: number }
| { type: "string"; value: string }
| { type: "binary"; op: "+" | "-" | "*" | "/"; left: Expression; right: Expression }
| { type: "call"; callee: string; args: Expression[] };

function evaluate(expr: Expression): number {
  switch (expr.type) {
    case "number": return expr.value;
    case "binary": {
      const l = evaluate(expr.left);
      const r = evaluate(expr.right);
      const ops = { "+": (a: number, b: number) => a + b, "-": (a: number, b: number) => a - b,
        "*": (a: number, b: number) => a * b, "/": (a: number, b: number) => a / b };
      return ops[expr.op](l, r);
    }
    default: throw new Error(`Cannot evaluate ${expr.type}`);
  }
}

Ошибка: бесконечная рекурсия в type alias без базового случая:

TypeScript
// ❌ Это не скомпилируется осмысленно
// type Infinite = Infinite[];

// ✅ Всегда нужен базовый случай (примитив, null, пустой массив)
type Finite = string | Finite[];

Резюме

Рекурсивные типы в TypeScript строятся через самоссылку в interface или type. Обязательно предусмотрите базовый случай (лист дерева), иначе структуру невозможно создать. Для сложных деревьев используйте discriminated union, чтобы различать типы узлов.

Как типизировать рекурсивную структуру данных (дерево)? | JScriptiser