Типизация кастомных компонентов

MUI (Material-UI) предоставляет мощный набор инструментов для создания компонентов с полной поддержкой TypeScript. Ключевая задача при разработке кастомных компонентов — корректная типизация пропсов, что позволяет сохранять совместимость с системами автокомплита, предотвращать ошибки и использовать преимущества строгой проверки типов.


Базовые подходы к типизации

При создании кастомного компонента на основе MUI важно учитывать два аспекта:

  1. Пропсы собственного компонента — это специфичные для вашей логики свойства.
  2. Пропсы компонента MUI, на котором основан ваш компонент — их следует наследовать, чтобы не терять функциональность.

Пример базового компонента с типизацией:

import React from 'react';
import Button, { ButtonProps } from '@mui/material/Button';

interface CustomButtonProps extends ButtonProps {
  highlight?: boolean;
}

const CustomButton: React.FC<CustomButtonProps> = ({ highlight, ...props }) => {
  return (
    <Button
      {...props}
      style={{
        backgroundColor: highlight ? 'yellow' : undefined,
        ...props.style,
      }}
    >
      {props.children}
    </Button>
  );
};

export default CustomButton;

Объяснение ключевых моментов:

  • extends ButtonProps позволяет автоматически использовать все свойства стандартного MUI Button (variant, color, size и т.д.).
  • Свойство highlight добавляется отдельно и учитывается при рендере.
  • Распаковка {...props} обеспечивает передачу всех стандартных пропсов внутрь компонента MUI.

Использование OverridableComponent для расширяемости

MUI предоставляет тип OverridableComponent, который позволяет создавать компоненты с динамическим component prop, что особенно важно для элементов вроде кнопок, ссылок или карточек.

Пример:

import { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent';
import { ButtonTypeMap } from '@mui/material/Button';

interface CustomButtonTypeMap<P = {}, D extends React.ElementType = 'button'> {
  props: P & { highlight?: boolean };
  defaultComponent: D;
}

type CustomButtonProps<
  D extends React.ElementType = CustomButtonTypeMap['defaultComponent'],
  P = {}
> = OverrideProps<CustomButtonTypeMap<P, D>, D>;

const CustomButton: OverridableComponent<CustomButtonTypeMap> = (props) => {
  const { highlight, ...rest } = props;
  return <Button {...rest} style={{ backgroundColor: highlight ? 'yellow' : undefined }} />;
};

Преимущества:

  • Поддержка component="a" или любого другого тега без потери типизации.
  • Возможность комбинировать пользовательские пропсы с пропсами MUI.

Использование styled и типизация стилей

При создании кастомных компонентов с использованием styled важно правильно типизировать пропсы, влияющие на стили:

import { styled } from '@mui/material/styles';
import Button, { ButtonProps } from '@mui/material/Button';

interface StyledButtonProps extends ButtonProps {
  highlight?: boolean;
}

const StyledButton = styled(Button, {
  shouldForwardProp: (prop) => prop !== 'highlight',
})<StyledButtonProps>(({ highlight, theme }) => ({
  backgroundColor: highlight ? theme.palette.warning.main : undefined,
  color: highlight ? theme.palette.common.black : undefined,
}));

export default StyledButton;

Особенности:

  • shouldForwardProp предотвращает передачу нестандартных пропсов в DOM.
  • Использование дженериков <StyledButtonProps> обеспечивает корректную проверку типов для пользовательских свойств.

Типизация сложных пропсов и функций

Для компонентов, которые принимают функции обратного вызова с определёнными типами, TypeScript позволяет точно описывать сигнатуры:

interface CustomInputProps {
  onValueChange?: (value: string, isValid: boolean) => void;
}

const CustomInput: React.FC<CustomInputProps> = ({ onValueChange }) => {
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const value = event.target.value;
    const isValid = value.length > 0;
    onValueChange?.(value, isValid);
  };

  return <input type="text" onCha nge={handleChange} />;
};
  • Типизация функций обратного вызова гарантирует, что параметры передаются строго в ожидаемом формате.
  • Использование опциональных пропсов (?) повышает гибкость без потери безопасности типов.

Наследование типов и композиция компонентов

Иногда требуется комбинировать несколько компонентов с разными типами пропсов. Для этого удобно использовать пересечение типов:

interface ExtraProps {
  tooltip?: string;
}

type ComposedButtonProps = CustomButtonProps & ExtraProps;

const ComposedButton: React.FC<ComposedButtonProps> = ({ tooltip, ...props }) => (
  <div title={tooltip}>
    <CustomButton {...props} />
  </div>
);
  • Пересечение типов (&) объединяет свойства нескольких интерфейсов.
  • Это позволяет создавать богатые кастомные компоненты, сохраняя поддержку автокомплита и проверки типов.

Полезные рекомендации по типизации

  • Использовать extends для наследования стандартных MUI пропсов.
  • Для компонентов с возможностью замены тега использовать OverridableComponent.
  • Для styled-компонентов применять <Props> и shouldForwardProp.
  • Всегда явно типизировать функции обратного вызова и сложные пропсы.
  • Пересечения интерфейсов помогают строить композицию компонентов без потери типизации.

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