$ sudo teach IT

Модуль 11: React + TypeScript

Урок 11.2: Типизация пропсов компонентов

Продвинутый уровень 55 минут

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

  1. Создайте интерфейс ModalProps с полями: isOpen (boolean), onClose () => void, title (string), children (React.ReactNode), size ('small' | 'medium' | 'large')
  2. Реализуйте компонент Modal с правильной типизацией
  3. Добавьте обработку закрытия по клику на оверлей
  4. Добавьте закрытие по нажатию Escape

Задание 2: Типизация формы

  1. Создайте интерфейс для данных формы регистрации (username, email, password, confirmPassword)
  2. Создайте компонент RegistrationForm с типизированными обработчиками событий
  3. Добавьте валидацию паролей (должны совпадать)
  4. Используйте generics для переиспользования компонента формы

Задание 3: Переиспользуемый компонент

  1. Создайте полиморфный компонент Box, который может рендериться как div, section, article, или aside
  2. Добавьте пропсы: padding, margin, backgroundColor, borderRadius
  3. Используйте generics для типизации тега
  4. Документируйте все пропсы с помощью JSDoc-комментариев

11. Итоги урока

В этом уроке мы подробно изучили:

  • Основы типизации пропсов с помощью interface и type
  • Деструктуризацию пропсов с правильной типизацией
  • Типизацию children (React.ReactNode, React.ReactElement)
  • Наследование пропсов от HTML-элементов
  • Использование generics для создания переиспользуемых компонентов
  • Типизацию обработчиков событий
  • Паттерны типизации (Composition, Polymorphic Components)
  • Утилитарные типы для работы с пропсами

Лучшие практики

  1. Всегда определяйте интерфейсы для пропсов, даже если они простые
  2. Используйте interface для объектов, type — для union-типов и кортежей
  3. Избегайте использования any — это подрывает преимущества TypeScript
  4. Используйте generics для создания переиспользуемых компонентов
  5. Расширяйте HTML-элементы для кастомных компонентов формы

В следующем уроке мы разберём типизацию состояния (useState) и управление состоянием в React-компонентах.

Отличная работа! Вы освоили типизацию пропсов компонентов!

Тест по типизации пропсов компонентов

1 вопрос

Тест по children и типизации

1 вопрос

Тест по наследованию пропсов

1 вопрос

Типизация пропсов через interface

Premium