Типы для MDX компонентов

MDX — это расширение Markdown, позволяющее использовать JSX внутри Markdown-документов. Важно правильно управлять типами компонентов, чтобы избежать ошибок во время компиляции и обеспечить корректную интеграцию с TypeScript.

Основные понятия

MDX-файлы обрабатываются как React-компоненты. Это означает, что каждый элемент Markdown, включая заголовки, списки и параграфы, можно переопределить через компоненты. Для типизации этих компонентов используется интерфейс MDXProviderComponents из пакета @mdx-js/react.

import { MDXProviderComponents } from '@mdx-js/react';

const components: MDXProviderComponents = {
  h1: (props) => <h1 style={{ color: 'red' }} {...props} />,
  p: (props) => <p style={{ fontSize: '16px' }} {...props} />,
};

Здесь MDXProviderComponents обеспечивает строгую типизацию ключей (теги Markdown) и их пропсов. Ключи должны соответствовать стандартным тегам HTML или пользовательским компонентам, а пропсы — JSX-параметрам этих компонентов.

Переопределение стандартных тегов

Каждый стандартный Markdown-элемент можно заменить пользовательским компонентом. Типизация позволяет указать точный интерфейс для каждого компонента:

interface CustomHeadingProps {
  children: React.ReactNode;
  level: 1 | 2 | 3 | 4 | 5 | 6;
}

const H: React.FC<CustomHeadingProps> = ({ children, level }) => {
  const Tag = `h${level}` as keyof JSX.IntrinsicElements;
  return <Tag>{children}</Tag>;
};

const components: MDXProviderComponents = {
  h1: (props) => <H level={1} {...props} />,
  h2: (props) => <H level={2} {...props} />,
};

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

Пользовательские компоненты

MDX позволяет добавлять полностью кастомные компоненты, которые можно использовать прямо в Markdown:

interface AlertProps {
  type: 'success' | 'error' | 'info';
  children: React.ReactNode;
}

const Alert: React.FC<AlertProps> = ({ type, children }) => (
  <div className={`alert alert-${type}`}>{children}</div>
);

const components: MDXProviderComponents = {
  Alert, // теперь можно использовать <Alert> внутри MDX
};

Типизация здесь помогает сразу видеть ошибки при неверном использовании пропсов (type="warning" будет ошибкой, так как тип отсутствует в интерфейсе).

Расширение типов MDX

Можно объединять стандартные HTML-компоненты и пользовательские через MDXComponents:

import { MDXComponents } from 'mdx/types';

interface CustomComponents extends MDXComponents {
  Alert: React.FC<AlertProps>;
}

const components: CustomComponents = {
  h1: (props) => <h1 style={{ color: 'blue' }} {...props} />,
  Alert,
};

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

Пропсы по умолчанию

MDX позволяет указывать базовые пропсы для компонентов, чтобы избежать дублирования кода в каждом вызове:

const DefaultParagraph: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = (props) => (
  <p style={{ lineHeight: '1.6', marginBottom: '1em' }} {...props} />
);

const components: MDXProviderComponents = {
  p: DefaultParagraph,
};

Типизация Children и других сложных пропсов

Для компонентов, принимающих children, рекомендуется использовать React.ReactNode или React.ReactElement в зависимости от того, требуется ли строгая вложенность:

interface CardProps {
  title: string;
  children: React.ReactNode;
}

const Card: React.FC<CardProps> = ({ title, children }) => (
  <div className="card">
    <h3>{title}</h3>
    <div>{children}</div>
  </div>
);

React.ReactNode позволяет передавать любой контент: текст, элементы JSX, массивы элементов.

Использование TypeScript для безопасного импорта MDX

MDX-файлы могут быть импортированы как React-компоненты, если настроена декларация модулей:

declare module '*.mdx' {
  let MDXComponent: (props: any) => JSX.Element;
  export default MDXComponent;
}

Можно усилить типизацию, указав пропсы для всех MDX-файлов:

declare module '*.mdx' {
  import { MDXProviderComponents } from '@mdx-js/react';
  interface MDXProps {
    components?: MDXProviderComponents;
  }
  const MDXComponent: React.FC<MDXProps>;
  export default MDXComponent;
}

Это позволяет передавать компоненты через components в MDX без потери типизации.

Практическая рекомендация

  • Всегда использовать MDXProviderComponents или расширять его интерфейсы для пользовательских компонентов.
  • Типизировать children и все props для предсказуемости поведения.
  • Переопределять HTML-теги только при необходимости, чтобы сохранять совместимость с стандартами Markdown.
  • Декларировать модуль для .mdx файлов, чтобы TypeScript понимал структуру и пропсы.

Эта система типизации обеспечивает безопасную работу с MDX и интеграцию с React, предотвращая ошибки еще на этапе компиляции и облегчая поддержку больших проектов.