Generic типы в unified

Библиотеки Remark и Rehype, построенные на основе экосистемы unified, используют систему типизации TypeScript для обеспечения строгой совместимости между различными плагинами и деревьями синтаксиса. Generic типы в unified позволяют точно определить структуру входных и выходных данных, а также специфицировать типы узлов (nodes) AST (Abstract Syntax Tree), обеспечивая безопасность при трансформациях.


Типизация процессора unified

Процессор в unified создается через функцию unified(), которая возвращает объект с методами .use(), .parse(), .stringify(), .run(), .runSync(). Типы этих методов зависят от generic-параметров процессора:

import { unified } from 'unified';
import { Node } from 'unist';

type MyNode = Node & { myProp: string };

const processor = unified<MyNode>();

Здесь MyNode задаёт тип AST, с которым будет работать процессор. Все последующие плагины будут ожидать именно этот тип, что предотвращает ошибки при обращении к нестандартным свойствам узлов.


Generic параметры в unified

Unified поддерживает до трёх generic-параметров:

unified<
  ParseTree = Node,       // Тип дерева после парсинга
  Input = string,         // Входной формат
  Output = Node,          // Тип дерева после трансформаций
  StringOutput = string   // Тип финального результата stringify
>()
  • ParseTree — результат функции .parse().
  • Input — тип входных данных для .process().
  • Output — результат после применения плагинов и .run().
  • StringOutput — тип результата метода .stringify().

Использование этих параметров обеспечивает строгую типизацию, которая помогает выявлять ошибки на этапе компиляции.


Пример применения generic типов

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';
import { Node } from 'unist';

interface MyMarkdownNode extends Node {
  type: 'paragraph' | 'text';
  value?: string;
}

const processor = unified<
  MyMarkdownNode,   // ParseTree
  string,           // Input
  MyMarkdownNode,   // Output
  string            // StringOutput
>()
  .use(remarkParse)
  .use(remarkStringify);

const markdown = 'Пример текста';
const processed = processor.processSync(markdown).toString();

В этом примере тип MyMarkdownNode применяется ко всем этапам обработки: парсинг, трансформация и генерация строки. Ошибки типа, например, обращение к несуществующему свойству value на узле другого типа, будут обнаружены на этапе компиляции.


Совмещение generic типов с плагинами

Плагины в unified также могут быть типизированы с помощью generic-параметров:

import { Transformer } from 'unified';
import { Node } from 'unist';

const myPlugin: Transformer<MyMarkdownNode, MyMarkdownNode> = (tree) => {
  tree.children.forEach((node) => {
    if (node.type === 'text' && node.value) {
      node.value = node.value.toUpperCase();
    }
  });
  return tree;
};

Здесь Transformer<ParseTree, OutputTree> гарантирует, что плагин будет принимать дерево одного типа и возвращать дерево того же типа или совместимого типа. Это особенно важно при построении цепочек из нескольких плагинов.


Строгая типизация узлов AST

В unified AST строится на основе стандарта unist, где каждый узел имеет обязательное поле type и массив children для вложенных узлов. Generic типы позволяют уточнять свойства узлов:

interface ParagraphNode extends Node {
  type: 'paragraph';
  children: TextNode[];
}

interface TextNode extends Node {
  type: 'text';
  value: string;
}

type MyAST = ParagraphNode | TextNode;

Использование union-типов для узлов AST обеспечивает автодополнение и проверки типов при обходе дерева:

const processor = unified<MyAST>();
const tree = processor.parse('Текст') as MyAST;

tree.children.forEach((node) => {
  if (node.type === 'text') {
    console.log(node.value.toUpperCase());
  }
});

Преимущества использования generic типов

  1. Безопасность типов — любые ошибки при работе с нестандартными свойствами узлов будут обнаружены на этапе компиляции.
  2. Автодополнение — TypeScript будет предлагать свойства конкретного узла в редакторе.
  3. Совместимость плагинов — плагины ожидают точно определённый тип дерева.
  4. Гибкость — возможность создавать собственные узлы с дополнительными свойствами без потери типовой безопасности.

Особенности при работе с Rehype

Rehype использует тот же API, что и Remark, но с деревом HTML (hast). Generic типы позволяют определить структуру HTML-узлов:

import { unified } from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';
import { Element, Root } from 'hast';

interface MyElement extends Element {
  tagName: 'p' | 'div';
}

type MyHAST = Root & { children: MyElement[] };

const processor = unified<MyHAST, string, MyHAST, string>()
  .use(rehypeParse)
  .use(rehypeStringify);

Тип MyHAST задаёт строгую структуру HTML-дерева, что позволяет безопасно обходить и модифицировать элементы без runtime ошибок.


Итоговая схема использования generic типов

  • unified<ParseTree, Input, Output, StringOutput> задаёт типы процессора.
  • Плагины используют Transformer<ParseTree, OutputTree> для типобезопасных трансформаций.
  • Узлы AST (unist/hast) типизируются через интерфейсы и union-типы.
  • Любая модификация дерева получает строгую проверку типов, что уменьшает ошибки при интеграции нескольких плагинов.

Generic типы являются фундаментом при построении сложных пайплайнов Remark и Rehype, обеспечивая одновременно гибкость и безопасность.