Типизация плагинов

Remark и Rehype являются мощными инструментами для работы с Markdown и HTML в экосистеме JavaScript. Одна из ключевых задач при работе с этими библиотеками — корректная типизация плагинов, что позволяет избегать ошибок и повышает предсказуемость поведения кода.


Основы типизации плагинов

Плагин в Remark или Rehype — это функция, которая может принимать опции и возвращать другую функцию, взаимодействующую с AST (Abstract Syntax Tree). Основные формы плагинов:

  1. Без опций:
import { Plugin } from 'unified';

const myPlugin: Plugin = () => {
  return (tree) => {
    // Обработка дерева
  };
};
  1. С опциями:
interface MyOptions {
  verbose: boolean;
}

const myPluginWithOptions: Plugin<[MyOptions]> = (options) => {
  return (tree) => {
    if (options.verbose) {
      console.log(tree);
    }
  };
};

Ключевой момент: тип Plugin может принимать кортеж аргументов [Options], что позволяет строго типизировать передаваемые параметры.


Типы AST и их использование

Remark использует mdast (Markdown AST), а Rehype — hast (HTML AST). Для корректной типизации плагина необходимо импортировать соответствующие типы:

import { Root } from 'mdast';
import { Plugin } from 'unified';

const plugin: Plugin<[{}], Root> = () => {
  return (tree) => {
    // tree имеет тип Root
  };
};
  • Root — корневой узел дерева.
  • Узлы могут быть конкретных типов: Heading, Paragraph, Text, Link.
  • Типизация позволяет автодополнение и проверку структуры дерева.

Пример обработки заголовков в Markdown:

import { visit } from 'unist-util-visit';
import { Root, Heading } from 'mdast';
import { Plugin } from 'unified';

const headingPlugin: Plugin<[], Root> = () => {
  return (tree) => {
    visit(tree, 'heading', (node: Heading) => {
      node.data = node.data || {};
      node.data.customId = node.children.map(c => 'value' in c ? c.value : '').join('-');
    });
  };
};

Типизация опций и кортежей

Remark и Rehype позволяют передавать несколько аргументов в плагин. Для типизации необходимо использовать кортежи:

interface MyPluginOptions {
  prefix: string;
  enabled: boolean;
}

const pluginWithTuple: Plugin<[MyPluginOptions, number]> = (options, repeat) => {
  return (tree) => {
    // options имеет тип MyPluginOptions
    // repeat имеет тип number
  };
};

Использование кортежей повышает строгость и предотвращает передачу лишних аргументов.


Обработка данных между плагинами

AST узлы могут содержать поле data, которое используется для хранения промежуточной информации между плагинами. Типизация data позволяет безопасно использовать его:

import { Node } from 'unist';

interface CustomData {
  customId?: string;
}

const plugin: Plugin<[], Node> = () => {
  return (tree) => {
    (tree.data as CustomData).customId = 'example';
  };
};

Без приведения типов TypeScript не сможет корректно распознать поле customId, поэтому часто используется as CustomData.


Типизация функций visit

Функция visit из пакета unist-util-visit позволяет обходить дерево AST. Ее типизация позволяет точно указать, с каким узлом работать:

import { visit } from 'unist-util-visit';
import { Heading } from 'mdast';

visit(tree, 'heading', (node: Heading) => {
  console.log(node.depth, node.children);
});
  • Второй аргумент — тип узла, который фильтруется.
  • Третий аргумент — callback с типом узла.

Плагины с асинхронной логикой

Remark и Rehype поддерживают асинхронные плагины. Типизация для асинхронных функций аналогична синхронной, но возвращается Promise<void>:

import { Plugin } from 'unified';
import { Root } from 'mdast';

const asyncPlugin: Plugin<[], Root> = async () => {
  return async (tree) => {
    await doSomething(tree);
  };
};

Типизация async-плагинов позволяет интегрировать внешние API или файловые операции без потери проверки типов.


Композиция плагинов

Унифицированная экосистема поддерживает цепочки плагинов:

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

unified()
  .use(remarkParse)
  .use(pluginWithTuple, [{ prefix: 'id', enabled: true }, 3])
  .use(remarkStringify);
  • TypeScript проверяет соответствие типов аргументов каждого плагина.
  • Ошибки типов выявляются на этапе компиляции, что снижает вероятность багов.

Советы по безопасной типизации

  1. Использовать интерфейсы для опций плагинов.
  2. Определять типы AST для каждого плагина (Root, Heading, Paragraph и т.д.).
  3. Использовать кортежи для передачи нескольких аргументов.
  4. Применять строгую типизацию поля data для передачи промежуточной информации.
  5. Всегда указывать тип плагина: Plugin<[Options], NodeType>.

Эти принципы позволяют создавать безопасные, читаемые и масштабируемые плагины для Remark и Rehype, полностью используя возможности TypeScript.