Модуль 11: React + TypeScript
Урок 11.2: Типизация пропсов компонентов
1. Основы типизации пропсов
Типизация пропсов — это фундаментальная часть работы с React и TypeScript. Правильная типизация обеспечивает безопасность кода, улучшает 경험 разработки в IDE и служит живой документацией для ваших компонентов.
В этом уроке мы детально разберём все способы типизации пропсов компонентов, от простых до сложных, и научимся выбирать подходящий метод для каждой ситуации.
Способы типизации пропсов
TypeScript предоставляет несколько способов типизации пропсов в React-компонентах:
1. Interface — наиболее предпочтительный способ для объектных пропсов
2. Type — альтернативный способ, полезный для union-типов и кортежей
3. Inline-типизация — быстрый способ для простых компонентов
4. React.FC — использование встроенного типа для функциональных компонентов
Способ 1: Использование Interface
Interface — это самый распространённый и рекомендуемый способ типизации пропсов. Он обеспечивает явное объявление структуры данных и поддерживает расширение через наследование.
// Определяем интерфейс для пропсов компонента Button
interface ButtonProps { label: string; // Обязательное строковое поле
onClick: () => void; // Обязательная функция без аргументов
variant?: 'primary' | 'secondary' | 'danger'; // Необязательный union-тип
disabled?: boolean; // Необязательное булево поле
size?: 'small' | 'medium' | 'large'; // Необязательный union-тип
} // Функциональный компонент с типизацией пропсов
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = 'primary', disabled = false, size = 'medium', }) => { return ( <button className={`btn btn-${variant} btn-${size}`} onClick={onClick} disabled={disabled} > {label} </button> ); }; export default Button;
Использование компонента Button:
// Правильное использование
<Button label="Нажми меня" onClick={() => alert('Привет!')} variant="primary" size="large" /> // С необязательными пропсами (используются значения по умолчанию)
<Button label="Сохранить" onClick={() => saveData()} /> // ❌ Ошибка: label обязателен
// <Button onClick={() => alert('Привет!')} />
// ❌ Ошибка: variant должен быть одним из допустимых значений
// <Button label="Кнопка" onClick={() => {}} variant="green" />
2. Использование Type для пропсов
Type (type alias) — это альтернативный способ определения типов. Он особенно полезен при работе с union-типами, кортежами и более сложными типовыми конструкциями.
Когда использовать Type вместо Interface
Используйте type когда:
- Нужно создать union-тип (Type | Type2 | Type3)
- Нужно создать кортеж (fixed-length array)
- Нужно использовать утилитарные типы (Partial, Pick, Omit)
- Нужно создать более сложные типовые конструкции
// Определяем типы для компонента Input
type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'; type InputSize = 'small' | 'medium' | 'large'; type ValidationState = 'valid' | 'invalid' | 'pending'; type InputProps = { type?: InputType; value: string; onChange: (value: string) => void; placeholder?: string; size?: InputSize; disabled?: boolean; error?: string; validationState?: ValidationState; icon?: React.ReactNode; }; const Input: React.FC<InputProps> = ({ type = 'text', value, onChange, placeholder, size = 'medium', disabled = false, error, validationState, icon, }) => { const inputClasses = [ 'input', `input-${size}`, error ? 'input-error' : '', validationState === 'valid' ? 'input-valid' : '', ].filter(Boolean).join(' '); return ( <div className="input-wrapper"> {icon && <span className="input-icon">{icon}</span>} <input type={type} value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} className={inputClasses} disabled={disabled} /> {error && <span className="input-error-text">{error}</span>} </div> ); };
3. Деструктуризация пропсов
Деструктуризация пропсов — это распространённый паттерн в React, который позволяет сразу извлекать нужные значения из объекта пропсов. TypeScriptидеально работает с деструктуризацией.
Правильная типизация деструктуризации
// Способ 1: Деструктуризация в параметрах функции (наиболее распространённый)
interface CardProps { title: string; description: string; imageUrl?: string; onClick?: () => void; } const Card: React.FC<CardProps> = ({ title, description, imageUrl, onClick, }) => { return ( <div className="card" onClick={onClick}> {imageUrl && <img src={imageUrl} alt={title} />} <h3>{title}</h3> <p>{description}</p> </div> ); }; // Способ 2: Деструктуризация с начальными значениями
interface AvatarProps { src: string; alt: string; size?: 'small' | 'medium' | 'large'; rounded?: boolean; } const Avatar: React.FC<AvatarProps> = ({ src, alt, size = 'medium', rounded = false, }) => { const sizeClasses = { small: 'avatar-sm', medium: 'avatar-md', large: 'avatar-lg', }; return ( <img src={src} alt={alt} className={`avatar ${sizeClasses[size]} ${rounded ? 'avatar-rounded' : ''}`} /> ); };
Продвинутая деструктуризация с типизацией
Иногда вам может понадобиться деструктурировать вложенные объекты или использовать переименование переменных:
interface UserProfileProps { user: { name: string; email: string; avatar: string; role: 'admin' | 'user' | 'guest'; }; showEmail?: boolean; onEdit: (userId: string) => void; } // Деструктуризация с вложенными объектами и переименованием
const UserProfile: React.FC<UserProfileProps> = ({ user: { name, email, avatar, role }, // Деструктуризация вложенного объекта
showEmail = false, onEdit, }) => { return ( <div className="user-profile"> <img src={avatar} alt={name} /> <h3>{name}</h3> {showEmail && <p>{email}</p>} <span className={`role-${role}`}>{role}</span> </div> ); };
4. Дети (children) и их типизация
Поле children — это особый пропс в React, который позволяет передавать вложенные элементы в компонент. TypeScript предоставляет несколько способов типизации children.
Способы типизации children
// Способ 1: React.ReactNode (самый гибкий)
interface ContainerProps { children: React.ReactNode; title?: string; } const Container: React.FC<ContainerProps> = ({ children, title, }) => { return ( <div className="container"> {title && <h2>{title}</h2>} <div className="container-content"> {children} </div> </div> ); }; // Способ 2: React.ReactElement (только JSX-элементы)
interface WrapperProps { children: React.ReactElement; } const Wrapper: React.FC<WrapperProps> = ({ children, }) => { return ( <div className="wrapper"> {children} </div> ); }; // Способ 3: Массив элементов
interface ListProps { children: React.ReactNode[]; } const List: React.FC<ListProps> = ({ children, }) => { return ( <ul className="list"> {children.map((child, index) => ( <li key={index}>{child}</li> ))} </ul> ); }; // Способ 4: Функция render props
interface DataFetcherProps { url: string; children: (data: any, loading: boolean) => React.ReactNode; } const DataFetcher: React.FC<DataFetcherProps> = ({ url, children, }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }); }, [url]); return <>{children(data, loading)}</>; };
5. Extending пропсов (наследование)
Одна из мощных возможностей TypeScript — наследование интерфейсов. Это позволяет создавать компоненты, которые расширяют базовые HTML-элементы, добавляя кастомные пропсы.
Расширение HTML-элементов
// Расширение HTML-элемента Button
interface CustomButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'secondary' | 'danger'; size?: 'small' | 'medium' | 'large'; loading?: boolean; } const CustomButton: React.FC<CustomButtonProps> = ({ variant = 'primary', size = 'medium', loading = false, children, disabled, ...rest // Все остальные пропсы Button HTML
}) => { return ( <button className={`custom-btn custom-btn-${variant} custom-btn-${size}`} disabled={disabled || loading} {...rest} > {loading ? 'Загрузка...' : children} </button> ); }; // Теперь CustomButton поддерживает все атрибуты HTML-кнопки
<CustomButton variant="danger" size="large" type="submit" // Стандартный HTML-атрибут
form="my-form" // Стандартный HTML-атрибут
onClick={() => handleDelete()} > Удалить </CustomButton> // Расширение HTML-элемента Input
interface EnhancedInputProps extends React.InputHTMLAttributes<HTMLInputElement> { label?: string; error?: string; helperText?: string; } const EnhancedInput: React.FC<EnhancedInputProps> = ({ label, error, helperText, ...inputProps }) => { return ( <div className="enhanced-input"> {label && <label>{label}</label>} <input {...inputProps} className={error ? 'input-error' : ''} /> {error && <span className="error">{error}</span>} {helperText && <span className="helper">{helperText}</span>} </div> ); };
6. generics для пропсов
Generics (обобщённые типы) позволяют создавать переиспользуемые компоненты, которые работают с различными типами данных. Это одна из самых мощных возможностей TypeScript в контексте React.
Пример: переиспользуемый список
// Универсальный компонент списка с поддержкой generics
interface GenericListProps<T> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T) => string; emptyMessage?: string; } function GenericList<T>({ items, renderItem, keyExtractor, emptyMessage = 'Нет элементов', }: GenericListProps<T>): React.ReactElement { if (items.length === 0) { return <div className="empty">{emptyMessage}</div>; } return ( <ul className="generic-list"> {items.map((item, index) => ( <li key={keyExtractor(item)}> {renderItem(item, index)} </li> ))} </ul> ); } // Использование с типом User
interface User { id: number; name: string; email: string; } const users: User[] = [ { id: 1, name: 'Анна', email: 'anna@example.com' }, { id: 2, name: 'Борис', email: 'boris@example.com' }, ]; <GenericList items={users} renderItem={(user) => ( <div> <strong>{user.name}</strong> <span> - {user.email}</span> </div> )} keyExtractor={(user) => String(user.id)} />
Generics с ограничениями
// Generic с ограничением: T должен иметь поле id
interface HasId { id: string | number; } interface SelectableListProps<T extends HasId> { items: T[]; selectedId?: string | number; onSelect: (id: string | number) => void; renderItem: (item: T, isSelected: boolean) => React.ReactNode; } function SelectableList<T extends HasId>({ items, selectedId, onSelect, renderItem, }: SelectableListProps<T>): React.ReactElement { return ( <ul> {items.map((item) => ( <li key={item.id} onClick={() => onSelect(item.id)} className={item.id === selectedId ? 'selected' : ''} > {renderItem(item, item.id === selectedId)} </li> ))} </ul> ); }
7. Обработка событий в пропсах
Типизация обработчиков событий — одна из самых важных частей работы с React и TypeScript. Давайте разберём основные типы событий и как их правильно типизировать.
Основные типы событий React
React.ChangeEvent<T> — событие изменения значения (input, select, textarea)
React.FormEvent<T> — событие отправки формы
React.MouseEvent<T> — событие клика мыши
React.KeyboardEvent<T> — событие нажатия клавиши
React.FocusEvent<T> — событие фокуса
React.TouchEvent<T> — событие касания (для мобильных)
React.DragEvent<T> — событие перетаскивания
// Пример компонента с типизированными обработчиками событий
interface LoginFormProps { onSubmit: (email: string, password: string) => void; onError?: (message: string) => void; } const LoginForm: React.FC<LoginFormProps> = ({ onSubmit, onError, }) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); // Типизация обработчика изменения input
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value); }; // Типизация обработчика изменения textarea
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value); }; // Типизация обработчика отправки формы
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); if (!email || !password) { onError?.('Заполните все поля'); return; } onSubmit(email, password); }; return ( <form onSubmit={handleSubmit}> <input type="email" value={email} onChange={handleEmailChange} placeholder="Email" /> <input type="password" value={password} onChange={handlePasswordChange} placeholder="Пароль" /> <button type="submit">Войти</button> </form> ); };
8. Паттерны типизации пропсов
Существует несколько проверенных паттернов типизации пропсов, которые помогут сделать ваш код более чистым и поддерживаемым.
Паттерн 1: Composition (Составление)
// Паттерн: Компоненты-обёртки с общей логикой
interface BaseFieldProps { label: string; required?: boolean; error?: string; helperText?: string; } // Базовый компонент поля
const BaseField: React.FC<BaseFieldProps & { children: React.ReactNode }> = ({ label, required, error, helperText, children, }) => { return ( <div className="field"> <label> {label} {required && <span className="required">*</span>} </label> {children} {error && <span className="error">{error}</span>} {helperText && <span className="helper">{helperText}</span>} </div> ); }; // Конкретные компоненты полей
interface TextFieldProps extends BaseFieldProps { value: string; onChange: (value: string) => void; type?: 'text' | 'password' | 'email'; } const TextField: React.FC<TextFieldProps> = ({ value, onChange, type = 'text', ...fieldProps }) => { return ( <BaseField {...fieldProps}> <input type={type} value={value} onChange={(e) => onChange(e.target.value)} /> </BaseField> ); };
Паттерн 2: Polymorphic Components (Полиморфные компоненты)
// Полиморфный компонент: рендерит разные HTML-элементы
type TextVariant = 'h1' | 'h2' | 'h3' | 'p' | 'span'; interface TextProps<V extends TextVariant = 'p'> { variant?: V; children: React.ReactNode; color?: 'primary' | 'secondary' | 'muted'; weight?: 'normal' | 'medium' | 'bold'; } const Text: React.FC<TextProps> = ({ variant = 'p', children, color = 'primary', weight = 'normal', }) => { const Tag = variant; return ( <Tag className={`text text-${color} text-${weight}`} > {children} </Tag> ); }; // Использование:
<Text variant="h1" weight="bold">Заголовок</Text> <Text variant="p" color="muted">Подпись</Text>
9. Утилитарные типы для пропсов
TypeScript предоставляет множество встроенных утилитарных типов, которые очень полезны при работе с пропсами компонентов.
interface User { id: number; name: string; email: string; avatar: string; role: 'admin' | 'user'; } // Partial - делает все поля необязательными
type PartialUser = Partial<User>; // { id?: number; name?: string; email?: string; ... }
// Required - делает все поля обязательными
type RequiredUser = Required<User>; // Pick - выбирает только указанные поля
type UserBasic = Pick<User, 'id' | 'name'>; // { id: number; name: string; }
// Omit - исключает указанные поля
type UserWithoutAvatar = Omit<User, 'avatar'>; // Record - создаёт тип с фиксированными ключами
type RolePermissions = Record<User['role'], string[]>; // { admin: string[]; user: string[]; }
// ReturnType - получает тип возвращаемого значения функции
const getUser = (): User => { ... }; type GetUserReturn = ReturnType<typeof getUser>; // Parameters - получает типы параметров функции
const createUser = (name: string, email: string): User => { ... }; type CreateUserParams = Parameters<typeof createUser>; // [string, string]
Практическое применение утилитарных типов
// Компонент редактирования профиля - используем Partial
interface EditProfileProps { user: User; onSave: (updates: Partial<User>) => void; } const EditProfile: React.FC<EditProfileProps> = ({ user, onSave, }) => { const [name, setName] = useState(user.name); const [email, setEmail] = useState(user.email); const handleSubmit = () => { // Partial позволяет передать только изменённые поля
onSave({ name, email, }); }; return ( ... ); };
10. Практическое задание
Выполните следующие задания для закрепления материала:
Задание 1: Компонент Modal
- Создайте интерфейс ModalProps с полями: isOpen (boolean), onClose () => void, title (string), children (React.ReactNode), size ('small' | 'medium' | 'large')
- Реализуйте компонент Modal с правильной типизацией
- Добавьте обработку закрытия по клику на оверлей
- Добавьте закрытие по нажатию Escape
Задание 2: Типизация формы
- Создайте интерфейс для данных формы регистрации (username, email, password, confirmPassword)
- Создайте компонент RegistrationForm с типизированными обработчиками событий
- Добавьте валидацию паролей (должны совпадать)
- Используйте generics для переиспользования компонента формы
Задание 3: Переиспользуемый компонент
- Создайте полиморфный компонент Box, который может рендериться как div, section, article, или aside
- Добавьте пропсы: padding, margin, backgroundColor, borderRadius
- Используйте generics для типизации тега
- Документируйте все пропсы с помощью JSDoc-комментариев
11. Итоги урока
В этом уроке мы подробно изучили:
- Основы типизации пропсов с помощью interface и type
- Деструктуризацию пропсов с правильной типизацией
- Типизацию children (React.ReactNode, React.ReactElement)
- Наследование пропсов от HTML-элементов
- Использование generics для создания переиспользуемых компонентов
- Типизацию обработчиков событий
- Паттерны типизации (Composition, Polymorphic Components)
- Утилитарные типы для работы с пропсами
Лучшие практики
- Всегда определяйте интерфейсы для пропсов, даже если они простые
- Используйте interface для объектов, type — для union-типов и кортежей
- Избегайте использования any — это подрывает преимущества TypeScript
- Используйте generics для создания переиспользуемых компонентов
- Расширяйте HTML-элементы для кастомных компонентов формы
В следующем уроке мы разберём типизацию состояния (useState) и управление состоянием в React-компонентах.
Отличная работа! Вы освоили типизацию пропсов компонентов!
Тест по типизации пропсов компонентов
1 вопрос
Тест по children и типизации
1 вопрос
Тест по наследованию пропсов
1 вопрос