Типизация в TypeScript

Radix UI предоставляет низкоуровневые, полностью управляемые компоненты для React, которые легко интегрируются с любыми стилизующими решениями. Основная цель типизации в TypeScript — обеспечить строгую проверку свойств компонентов и их правильное использование в проекте, избегая ошибок во время компиляции.

Компоненты Radix UI, как правило, используют forwardRef для передачи рефа, что важно учитывать при типизации, особенно при создании оберток или кастомных компонентов.

import * as React from "react";
import * as Dialog from "@radix-ui/react-dialog";

const MyDialogTrigger = React.forwardRef<
  HTMLButtonElement,
  Dialog.DialogTriggerProps
>((props, ref) => <button ref={ref} {...props} />);

В этом примере тип Dialog.DialogTriggerProps гарантирует, что все стандартные свойства для триггера диалога доступны, а ref типизирован как HTMLButtonElement.


Типизация пропсов и рефов

Radix UI использует собственные интерфейсы пропсов для каждого компонента. Например, для Dialog:

  • DialogRootProps — пропсы корневого компонента диалога.
  • DialogTriggerProps — пропсы триггера открытия диалога.
  • DialogContentProps — пропсы контента диалога.

Типизация позволяет безопасно расширять компоненты без потери автокомплита:

interface CustomDialogProps extends Dialog.DialogContentProps {
  title: string;
  footer?: React.ReactNode;
}

const CustomDialog = React.forwardRef<HTMLDivElement, CustomDialogProps>(
  ({ title, footer, ...props }, ref) => (
    <Dialog.Content ref={ref} {...props}>
      <h2>{title}</h2>
      {props.children}
      {footer && <footer>{footer}</footer>}
    </Dialog.Content>
  )
);

Здесь расширение интерфейса DialogContentProps добавляет новые пропсы title и footer, сохраняя полную совместимость с Radix UI.


Использование Generic типов для расширяемых компонентов

Для создания обобщённых компонентов с Radix UI удобно использовать generic-типы. Например, если компонент должен работать с разными HTML-элементами:

interface ButtonProps<T extends React.ElementType> {
  as?: T;
  children: React.ReactNode;
}

const Button = <T extends React.ElementType = "button">({
  as,
  children,
  ...props
}: ButtonProps<T> & React.ComponentPropsWithoutRef<T>) => {
  const Component = as || "button";
  return <Component {...props}>{children}</Component>;
};

Такой подход позволяет использовать компонент как <Button as="a" href="/home">, сохраняя типизацию для всех стандартных HTML-пропсов.


Типизация событий

Radix UI предоставляет типы для событий, чтобы избежать использования any. Например, у DropdownMenu:

import * as DropdownMenu from "@radix-ui/react-dropdown-menu";

function handleSelect(event: DropdownMenu.DropdownMenuItemSelectEvent) {
  console.log(event);
}

Тип DropdownMenuItemSelectEvent гарантирует наличие всех полей события, специфичных для компонента.


Работа с Polymorphic Components

Radix UI допускает создание полиморфных компонентов через asChild:

import * as Dialog from "@radix-ui/react-dialog";

<Dialog.Trigger asChild>
  <button className="btn">Открыть диалог</button>
</Dialog.Trigger>

При этом DialogTriggerProps совместим с любым дочерним элементом, и TypeScript корректно проверяет пропсы и рефы.

Для типизации таких случаев удобно использовать утилиты:

type PolymorphicRef<C extends React.ElementType> = React.ComponentPropsWithRef<C>["ref"];

type PolymorphicProps<C extends React.ElementType, Props = {}> = Props &
  Omit<React.ComponentPropsWithoutRef<C>, keyof Props> & {
    as?: C;
  };

Эти утилиты позволяют создавать компоненты с as и строгой типизацией пропсов, сохраняя совместимость с Radix UI.


Интеграция с CSS-in-JS и Tailwind

Типизация Radix UI напрямую поддерживает передачу классов и стилей:

import * as Toast from "@radix-ui/react-toast";

const CustomToast = React.forwardRef<HTMLDivElement, Toast.ToastProps>(
  ({ className, ...props }, ref) => (
    <Toast.Root ref={ref} className={`bg-gray-800 text-white p-4 ${className}`} {...props} />
  )
);

ToastProps гарантируют правильность всех передаваемых событий и состояний, а className можно расширять без потери типизации.


Практические советы по типизации Radix UI

  1. Всегда использовать пропсы из Radix UI, а не дублировать типы вручную.
  2. Использовать forwardRef для оберток, чтобы сохранить совместимость с внутренними рефами.
  3. Применять generic и polymorphic типы для расширяемых компонентов.
  4. Расширять существующие интерфейсы через interface или type, сохраняя автокомплит.
  5. Типизировать события через предоставленные Radix UI типы, избегая any.

Типизация в TypeScript для Radix UI — это инструмент не только для безопасности кода, но и для повышения читаемости, автокомплита и предсказуемости поведения компонентов в масштабных проектах.