Зачем типизировать props
React — наиболее популярная UI-библиотека (40% всех new фронтенд-проектов в 2024 по State of JS). Без TypeScript любой проект быстро превращается в хаос: «какие props принимает этот компонент?», «обязательно ли передавать color?», «можно ли передать null?». Ответ — в коде, который надо читать.
С TypeScript эти вопросы решаются автоматически: IDE показывает доступные props, типы значений, optional/required. Опечатки и неверные типы — ошибка компиляции, не runtime баг. По данным GitHub, проекты с TypeScript имеют на 15% меньше production-ошибок.
Common patterns
1. Базовый Props interface
type ButtonProps = {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
};
export function Button({
label,
onClick,
variant = 'primary',
disabled = false,
size = 'md',
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant} btn-${size}`}
>
{label}
</button>
);
}Все необязательные props имеют default values через деструктуризацию. Variant ограничен union type — IDE подскажет варианты.
2. С children (PropsWithChildren)
import { PropsWithChildren } from 'react';
type CardProps = PropsWithChildren<{
title: string;
footer?: React.ReactNode;
}>;
export function Card({ title, footer, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// Использование
<Card title="Hello" footer={<button>OK</button>}>
<p>Card content here</p>
</Card>3. Event handlers
type FormFieldProps = {
label: string;
value: string;
// Несколько способов типизации:
// 1. React types (предпочтительно)
onChange: React.ChangeEventHandler<HTMLInputElement>;
// 2. Inline event type
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
// 3. Простая функция
onValueChange?: (value: string) => void;
};
export function FormField({ label, value, onChange, onBlur, onValueChange }: FormFieldProps) {
return (
<label>
{label}
<input
value={value}
onChange={(e) => {
onChange(e);
onValueChange?.(e.target.value);
}}
onBlur={onBlur}
/>
</label>
);
}4. Расширение HTML props
// Принимаем все стандартные input attrs (placeholder, name, autoComplete...)
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
label: string;
error?: string;
};
export function Input({ label, error, ...inputProps }: InputProps) {
return (
<label className="form-input">
<span>{label}</span>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</label>
);
}
// Использование — все стандартные input props доступны
<Input
label="Email"
type="email"
placeholder="ivan@example.com"
required
autoComplete="email"
/>Используйте type для большинства props — он более гибкий (поддерживает union, intersection, mapped types). interface оставьте для public API библиотек где важна возможность расширения через declaration merging.— React TypeScript Cheatsheet, 2024
Advanced — generic компоненты
List с произвольным типом item
type ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
emptyMessage?: string;
keyExtractor?: (item: T) => string | number;
};
export function List<T>({
items,
renderItem,
emptyMessage = 'No items',
keyExtractor,
}: ListProps<T>) {
if (items.length === 0) return <p>{emptyMessage}</p>;
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor ? keyExtractor(item) : i}>
{renderItem(item, i)}
</li>
))}
</ul>
);
}
// Использование
type User = { id: number; name: string };
const users: User[] = [...];
<List<User>
items={users}
renderItem={(user) => <UserCard user={user} />}
keyExtractor={(user) => user.id}
/>
// TS выведет тип User автоматически — generic не обязателен
<List
items={users} // TS видит User[]
renderItem={(user) => <UserCard user={user} />} // user: User
/>Discriminated union для разных вариантов
type AlertProps =
| { type: 'error'; error: Error; message?: never }
| { type: 'info'; message: string; error?: never }
| { type: 'success'; message: string; error?: never };
export function Alert(props: AlertProps) {
if (props.type === 'error') {
// TS знает что есть error, нет message
return <div className="alert-error">{props.error.message}</div>;
}
// Здесь props.type === 'info' | 'success'
return (
<div className={`alert-${props.type}`}>
{props.message}
</div>
);
}
// IDE подскажет required field в зависимости от type:
<Alert type="error" error={new Error('Failed')} /> // ✅
<Alert type="info" message="Hello" /> // ✅
<Alert type="error" message="Hello" /> // ❌ ошибка компиляцииPolymorphic component (as prop)
type TextProps<T extends React.ElementType = 'p'> = {
as?: T;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'children'>;
export function Text<T extends React.ElementType = 'p'>({
as,
children,
...rest
}: TextProps<T>) {
const Component = as || 'p';
return <Component {...rest}>{children}</Component>;
}
// Использование
<Text>Default p</Text>
<Text as="h1" id="title">Heading</Text>
<Text as="span" className="inline">Inline text</Text>
<Text as="a" href="/about">Link</Text> // TS знает href — валидный для <a>forwardRef с типами
type InputProps = {
label: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ label, ...rest }, ref) => {
return (
<label>
{label}
<input ref={ref} {...rest} />
</label>
);
}
);
Input.displayName = 'Input';
// Использование
const inputRef = useRef<HTMLInputElement>(null);
<Input
ref={inputRef}
label="Name"
placeholder="Иван"
onChange={(e) => console.log(e.target.value)}
/>Подводные камни
- any — плохая идея. Если используете any для props, теряете все преимущества TypeScript. Лучше unknown с runtime-проверкой.
- defaultProps deprecated. В React 18+ используйте default values в деструктуризации, не Component.defaultProps.
- Children — необязательное по умолчанию. Если children обязательны — явно:
{ children: ReactNode }, без знака вопроса. - Optional + default = always defined.
{ color?: string }с default «blue» — внутри функции color всегда string, не undefined. - React.FC устарел. Не используйте:
const Button: React.FC<Props> = (props) => .... React-team рекомендует обычные функции с типами параметра. - Слишком сложные generics. Если interface больше 50 строк — пересмотрите дизайн компонента. Возможно нужно разделить на несколько.
- React TypeScript Cheatsheet. React TypeScript Cheatsheets. react-typescript-cheatsheet.netlify.app. 2024.
- React Documentation — TypeScript. React Team. react.dev/learn/typescript. 2024.
- Total TypeScript — React Components. Matt Pocock. totaltypescript.com. 2024.
