← Назад к вопросам
Как реализуешь кнопку с разными типами которая передает разные 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 для простых случаев.