← Назад к вопросам
Как можно типизировать сложные props когда компонент принимает сложные типы данных?
2.3 Middle🔥 152 комментариев
#TypeScript#React
Комментарии (2)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Типизация сложных Props в React/TypeScript
При работе с компонентами которые принимают сложные типы данных, важно правильно определить типы props. Это улучшает type safety, документацию кода и developer experience.
Проблема: Без типизации
// Плохо - никакой типизации
function UserProfile(props) {
// Неясно какие поля есть в props
return (
<div>
<h1>{props.name}</h1>
<p>{props.email}</p>
{/* Может быть ошибка на проде */}
</div>
);
}
// Или с any
function UserProfile(props: any) {
// IDE не поможет с автодополнением
// Нет проверки типов
return <div>{props.unknownField}</div>;
}
Базовая типизация
interface User {
id: string;
name: string;
email: string;
age: number;
avatar?: string;
}
interface UserProfileProps {
user: User;
onUpdate?: (user: User) => void;
loading?: boolean;
}
function UserProfile({ user, onUpdate, loading }: UserProfileProps) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<button onClick={() => onUpdate?.(user)} disabled={loading}>
{loading ? 'Saving...' : 'Update'}
</button>
</div>
);
}
Типизация массивов сложных объектов
Массив объектов с вложенными данными:
interface Comment {
id: string;
text: string;
author: User; // Вложенный объект
timestamp: Date;
replies: Comment[]; // Рекурсивный тип
}
interface CommentListProps {
comments: Comment[];
onReply: (parentId: string, text: string) => Promise<void>;
}
function CommentList({ comments, onReply }: CommentListProps) {
return (
<div>
{comments.map(comment => (
<div key={comment.id}>
<p>{comment.text}</p>
<span>By {comment.author.name}</span>
{comment.replies.length > 0 && (
<CommentList comments={comment.replies} onReply={onReply} />
)}
</div>
))}
</div>
);
}
Типизация функций (Callbacks)
interface Product {
id: string;
name: string;
price: number;
}
interface ProductFormProps {
initialProduct?: Product;
// Функция которая получает Product и ничего не возвращает
onSave: (product: Product) => void;
// Функция которая возвращает Promise
onDelete: (productId: string) => Promise<void>;
// Функция с опциональным параметром
onCancel?: () => void;
}
function ProductForm({
initialProduct,
onSave,
onDelete,
onCancel
}: ProductFormProps) {
const [product, setProduct] = useState<Product>(initialProduct || {
id: '',
name: '',
price: 0,
});
const handleSave = async () => {
onSave(product);
};
const handleDelete = async () => {
await onDelete(product.id);
};
return (
<form>
{/* форма */}
<button onClick={handleSave}>Save</button>
<button onClick={handleDelete}>Delete</button>
{onCancel && <button onClick={onCancel}>Cancel</button>}
</form>
);
}
Типизация с обобщенными типами (Generics)
Когда компонент работает с разными типами данных:
// Параметризированный компонент - TableProps<T>
interface TableProps<T> {
data: T[];
columns: Array<{
key: keyof T; // Ключ из типа T
label: string;
render?: (value: T[keyof T]) => React.ReactNode;
}>;
onRowClick?: (row: T) => void;
}
function Table<T extends { id: string }>({
data,
columns,
onRowClick
}: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map(col => <th key={col.key as string}>{col.label}</th>)}
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.id} onClick={() => onRowClick?.(row)}>
{columns.map(col => (
<td key={col.key as string}>
{col.render ? col.render(row[col.key]) : (row[col.key] as React.ReactNode)}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Использование с конкретным типом
interface User {
id: string;
name: string;
email: string;
}
const userColumns: TableProps<User>['columns'] = [
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email', render: (email) => <a href={`mailto:${email}`}>{email}</a> },
];
<Table<User>
data={users}
columns={userColumns}
onRowClick={(user) => console.log(user.id)}
/>
Типизация Union Types
Когда props могут быть разными вариантами:
type NotificationProps =
| { type: 'success'; message: string }
| { type: 'error'; message: string; retryable: boolean }
| { type: 'warning'; message: string; icon: React.ReactNode }
| { type: 'info'; message: string };
function Notification(props: NotificationProps) {
switch (props.type) {
case 'success':
// IDE знает что есть только type и message
return <div className="success">{props.message}</div>;
case 'error':
// IDE знает что есть retryable
return (
<div className="error">
{props.message}
{props.retryable && <button>Retry</button>}
</div>
);
case 'warning':
// IDE знает что есть icon
return <div className="warning">{props.icon} {props.message}</div>;
case 'info':
return <div className="info">{props.message}</div>;
}
}
// Использование - TypeScript проверит что нужно передать
<Notification type="error" message="Error" retryable={true} />
<Notification type="info" message="Info" /> // Error - retryable required for 'error'
Типизация с Discriminated Union
Для более сложных случаев:
type FormField =
| { kind: 'text'; name: string; value: string; maxLength?: number }
| { kind: 'number'; name: string; value: number; min?: number; max?: number }
| { kind: 'checkbox'; name: string; value: boolean }
| { kind: 'select'; name: string; value: string; options: string[] };
interface FormProps {
fields: FormField[];
onChangeField: (fieldName: string, newValue: any) => void;
}
function Form({ fields, onChangeField }: FormProps) {
return (
<form>
{fields.map(field => {
// Discriminator - 'kind' поле узко определяет тип
switch (field.kind) {
case 'text':
return (
<input
key={field.name}
type="text"
value={field.value}
maxLength={field.maxLength}
onChange={(e) => onChangeField(field.name, e.target.value)}
/>
);
case 'number':
return (
<input
key={field.name}
type="number"
value={field.value}
min={field.min}
max={field.max}
onChange={(e) => onChangeField(field.name, Number(e.target.value))}
/>
);
case 'checkbox':
return (
<input
key={field.name}
type="checkbox"
checked={field.value}
onChange={(e) => onChangeField(field.name, e.target.checked)}
/>
);
case 'select':
return (
<select
key={field.name}
value={field.value}
onChange={(e) => onChangeField(field.name, e.target.value)}
>
{field.options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
);
}
})}
</form>
);
}
Типизация Render Props
Когда компонент передает данные в функцию:
interface LoaderProps<T> {
// Функция которая получает данные и возвращает ReactNode
children: (data: T, loading: boolean, error?: Error) => React.ReactNode;
loader: () => Promise<T>;
}
function Loader<T>({ children, loader }: LoaderProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();
useEffect(() => {
setLoading(true);
loader()
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [loader]);
if (data === null && loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <>{children(data as T, loading, error)}</>;
}
// Использование
<Loader<User>
loader={() => fetch('/api/user').then(r => r.json())}
>
{(user, loading) => (
<div>
<h1>{user.name}</h1>
{loading && <p>Updating...</p>}
</div>
)}
</Loader>
Типизация HOC (Higher Order Components)
interface WithAuthProps {
user: User;
isAuthenticated: boolean;
}
function withAuth<P extends WithAuthProps>(
Component: React.ComponentType<P>
): React.FC<Omit<P, keyof WithAuthProps>> {
return (props) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
if (!user) return <div>Loading...</div>;
return (
<Component
{...(props as P)}
user={user}
isAuthenticated={true}
/>
);
};
}
// Использование
interface MyComponentProps extends WithAuthProps {
title: string;
}
function MyComponent({ user, isAuthenticated, title }: MyComponentProps) {
return <div>{title} - {user.name}</div>;
}
const MyComponentWithAuth = withAuth(MyComponent);
// Теперь MyComponentWithAuth требует только title
<MyComponentWithAuth title="My Page" />
Best Practices
- Используй Interface для props - точнее и удобнее
- Разделяй обязательные и опциональные props -
?для опциональных - Используй Generics для переиспользуемых компонентов - Table, List, Modal
- Discriminated Union для вариантов - более типобезопасно чем enum
- Экспортируй типы props - чтобы другие компоненты могли их использовать
// Экспортируй типы
export interface ButtonProps {
variant: 'primary' | 'secondary';
children: React.ReactNode;
}
export function Button(props: ButtonProps) { }
Вывод
Правильная типизация props - это основа надежного React приложения. TypeScript предоставляет мощные инструменты (Generics, Union Types, Discriminated Union) для типизации любого уровня сложности. Затрати время на типы - сэкономишь время на отладку.