← Назад к вопросам

Как реализуешь кнопку с разными типами которая передает разные props для разных типов?

2.0 Middle🔥 221 комментариев
#React#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Типизированная кнопка с разными типами

Это классическая задача на использование TypeScript discriminated unions (типы, различаемые по полю). Есть несколько подходов - от простого к сложному.

Подход 1: Простой и понятный (discriminated union)

// types/Button.ts
type PrimaryButtonProps = {
  variant: "primary";
  color?: "blue" | "red" | "green";
  size?: "sm" | "md" | "lg";
  onClick: () => void;
  children: React.ReactNode;
};

type SecondaryButtonProps = {
  variant: "secondary";
  outline?: boolean;
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;
};

type IconButtonProps = {
  variant: "icon";
  icon: React.ReactNode;
  ariaLabel: string; // Требуется для доступности
  onClick: () => void;
};

type ButtonProps = PrimaryButtonProps | SecondaryButtonProps | IconButtonProps;

// components/Button.tsx
export function Button(props: ButtonProps) {
  switch (props.variant) {
    case "primary":
      return (
        <button
          className={`px-4 py-2 bg-${props.color || "blue"}-500 text-white rounded-lg`}
          onClick={props.onClick}
        >
          {props.children}
        </button>
      );
      
    case "secondary":
      return (
        <button
          className={`px-4 py-2 border border-gray-300 rounded-lg ${props.outline ? "bg-transparent" : "bg-gray-100"}`}
          onClick={props.onClick}
          disabled={props.disabled}
        >
          {props.children}
        </button>
      );
      
    case "icon":
      return (
        <button
          className="p-2 hover:bg-gray-100 rounded-lg transition"
          onClick={props.onClick}
          aria-label={props.ariaLabel}
        >
          {props.icon}
        </button>
      );
  }
}

Подход 2: С использованием generics (более продвинутый)

// Каждый вариант кнопки определяет свой набор props
type ButtonVariant<V extends string, P extends Record<string, any>> = {
  variant: V;
} & P;

type ButtonProps =
  | ButtonVariant<"primary", {
      color?: "blue" | "red";
      size?: "sm" | "lg";
      onClick: () => void;
      children: string;
    }>
  | ButtonVariant<"secondary", {
      disabled?: boolean;
      onClick?: () => void;
      children: React.ReactNode;
    }>
  | ButtonVariant<"icon", {
      icon: React.ReactNode;
      ariaLabel: string;
      onClick: () => void;
    }>;

export function Button(props: ButtonProps): React.ReactNode {
  const baseClasses = "transition-all duration-200";
  
  if (props.variant === "primary") {
    return (
      <button
        className={`${baseClasses} px-4 py-2 bg-${props.color || "blue"}-500`}
        onClick={props.onClick}
      >
        {props.children}
      </button>
    );
  }
  
  if (props.variant === "secondary") {
    return (
      <button
        className={`${baseClasses} px-4 py-2 border border-gray-300`}
        onClick={props.onClick}
        disabled={props.disabled}
      >
        {props.children}
      </button>
    );
  }
  
  // TypeScript знает, что это "icon"
  return (
    <button
      className={`${baseClasses} p-2 hover:bg-gray-100`}
      onClick={props.onClick}
      aria-label={props.ariaLabel}
    >
      {props.icon}
    </button>
  );
}

Подход 3: С асинхронными операциями

Если разные типы кнопок требуют разную обработку асинхронных операций:

type BaseButtonProps = {
  children: React.ReactNode;
  className?: string;
};

type PrimaryAsyncButtonProps = BaseButtonProps & {
  variant: "primary-async";
  onSubmit: () => Promise<void>;
  loadingText?: string;
};

type ConfirmButtonProps = BaseButtonProps & {
  variant: "confirm";
  onConfirm: () => void;
  title?: string;
  message?: string;
};

type ButtonProps = PrimaryAsyncButtonProps | ConfirmButtonProps;

export function Button(props: ButtonProps) {
  const [isLoading, setIsLoading] = React.useState(false);
  
  if (props.variant === "primary-async") {
    const handleClick = async () => {
      setIsLoading(true);
      try {
        await props.onSubmit();
      } catch (error) {
        console.error("Error:", error);
      } finally {
        setIsLoading(false);
      }
    };
    
    return (
      <button
        onClick={handleClick}
        disabled={isLoading}
        className={props.className}
      >
        {isLoading ? props.loadingText || "Loading..." : props.children}
      </button>
    );
  }
  
  if (props.variant === "confirm") {
    const handleClick = () => {
      if (window.confirm(props.message || "Are you sure?")) {
        props.onConfirm();
      }
    };
    
    return (
      <button onClick={handleClick} className={props.className}>
        {props.children}
      </button>
    );
  }
}

Практический пример использования

// Правильное использование - TypeScript проверит типы

// OK: primary button требует onClick
<Button variant="primary" color="blue" onClick={() => {}} />

// ERROR: secondary button не требует color
<Button variant="secondary" color="blue" /> // ошибка типа!

// OK: icon button требует ariaLabel
<Button variant="icon" icon={<IconComponent />} ariaLabel="Close" onClick={() => {}} />

// ERROR: primary button требует children, но secondary нет
<Button variant="secondary" /> // ошибка! children обязателен

Best Practices

1. Всегда использовать discriminated unions вместо одного большого interface:

// ❌ Плохо - все props в одном месте
interface ButtonProps {
  variant: "primary" | "secondary" | "icon";
  color?: string;
  size?: string;
  icon?: React.ReactNode;
  ariaLabel?: string;
  // Много опциональных props, непонятно какие требуются
}

// ✅ Хорошо - каждый вариант определяет свои props
type ButtonProps = 
  | (BaseProps & { variant: "primary"; color: string })
  | (BaseProps & { variant: "secondary" })
  | (BaseProps & { variant: "icon"; ariaLabel: string });

2. Типизируй обработчики событий:

// ✅ Хорошо
type PrimaryButtonProps = {
  variant: "primary";
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
};

// Так TypeScript предоставит автодополнение для event

3. Используй const assertions для литералов:

// ✅ Хорошо - TypeScript точно знает тип
const buttonConfig = {
  variant: "primary" as const,
  color: "blue" as const,
} satisfies PrimaryButtonProps;

Альтернатива: Composition вместо типов

Если типизация становится слишком сложной, используй composition:

// Простые компоненты
export function PrimaryButton({ children, onClick }: PrimaryProps) {
  return <button onClick={onClick}>{children}</button>;
}

export function SecondaryButton({ children }: SecondaryProps) {
  return <button>{children}</button>;
}

export function IconButton({ icon, ariaLabel, onClick }: IconProps) {
  return <button aria-label={ariaLabel} onClick={onClick}>{icon}</button>;
}

// Использование - просто импортируешь нужный компонент
<PrimaryButton onClick={() => {}} />
<SecondaryButton />
<IconButton ariaLabel="Close" icon={<X />} />

Выбирай discriminated unions для сложных типов с разными props, и composition для простых случаев.