Вы уже знаете TypeScript. В React он особенно ценен: компоненты принимают пропсы, возвращают JSX, работают с событиями — всё это можно строго типизировать. Результат: автодополнение в IDE, ошибки на этапе компиляции, самодокументирующийся код.
Предпочтительный подход — функция с явной типизацией пропсов:
// Объявляем интерфейс для пропсов
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary' | 'danger' // опциональный с union type
disabled?: boolean
children?: React.ReactNode // принимает любой JSX
}
// Явная типизация — предпочтительный стиль
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
// React.FC — устаревший стиль (не рекомендуется в React 18+):
// неявно добавлял children (исправлено), хуже с generics
const Button2: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>function Form() {
// Тип события: React.ChangeEvent<HTMLInputElement>
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value) // строка
console.log(e.target.checked) // boolean (для checkbox)
console.log(e.target.files) // FileList | null (для file input)
}
// Тип для форм: React.FormEvent<HTMLFormElement>
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
// Тип для кнопок: React.MouseEvent<HTMLButtonElement>
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log(e.currentTarget.dataset.id)
}
// Тип для клавиатуры: React.KeyboardEvent<HTMLInputElement>
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submitForm()
}
}// useState: тип выводится из initialValue или явный generic
const [name, setName] = useState('') // string (вывод)
const [count, setCount] = useState(0) // number (вывод)
const [user, setUser] = useState<User | null>(null) // явный тип необходим!
// useRef: тип DOM-элемента
const inputRef = useRef<HTMLInputElement>(null)
// ref.current может быть null до монтирования — нужна проверка
inputRef.current?.focus()
// useRef для мутабельного значения (не DOM)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
// useReducer: типизация action через discriminated union
type Action =
| { type: 'increment' }
| { type: 'add'; payload: number }
| { type: 'reset'; payload: number }
interface State { count: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'add': return { count: state.count + action.payload }
case 'reset': return { count: action.payload }
}
}interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
// null как дефолт — защита от использования вне провайдера
const ThemeContext = createContext<ThemeContextType | null>(null)
// Кастомный хук с защитой
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme должен использоваться внутри ThemeProvider')
return ctx
}// Компонент, работающий с любым типом данных
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string | number
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
)
}
// Использование — TypeScript выводит тип T автоматически
<List
items={users}
keyExtractor={(u) => u.id} // TypeScript знает: u — User
renderItem={(u) => u.name}
/>interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
// forwardRef<ТипЭлемента, ТипПропсов>
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)// Вместо enum — as const (легче, лучше совместимость)
const TODO_ACTIONS = {
ADD: 'ADD_TODO',
TOGGLE: 'TOGGLE_TODO',
DELETE: 'DELETE_TODO',
} as const
type TodoActionType = typeof TODO_ACTIONS[keyof typeof TODO_ACTIONS]
// 'ADD_TODO' | 'TOGGLE_TODO' | 'DELETE_TODO'Типизированные паттерны React на TypeScript: props interface, события, discriminated union для reducer, generic компоненты
// Этот файл демонстрирует TypeScript-паттерны для React.
// Запустить в браузере нельзя, но код показывает реальные типы.
// === 1. Типизация компонента с пропсами ===
// interface ButtonProps {
// label: string
// onClick: () => void
// variant?: 'primary' | 'secondary' | 'danger'
// icon?: React.ReactNode
// }
//
// function Button({ label, onClick, variant = 'primary', icon }: ButtonProps) {
// return (
// <button className={`btn-${variant}`} onClick={onClick}>
// {icon && <span className="icon">{icon}</span>}
// {label}
// </button>
// )
// }
// Эмулируем проверку типов в чистом JS:
function createTypeSafeComponent(defaultProps) {
return function render(props) {
const merged = { ...defaultProps, ...props }
// Валидация типов в runtime (что делает TypeScript на этапе компиляции)
const required = ['label', 'onClick']
required.forEach(key => {
if (!(key in merged)) throw new Error(`Prop "${key}" обязателен!`)
})
const variantOptions = ['primary', 'secondary', 'danger']
if (merged.variant && !variantOptions.includes(merged.variant)) {
throw new Error(`variant должен быть одним из: ${variantOptions.join(', ')}`)
}
console.log(' Рендер Button:', merged.label, '| variant:', merged.variant)
return merged
}
}
const Button = createTypeSafeComponent({ variant: 'primary' })
// === 2. Discriminated Union для reducer ===
// TypeScript:
// type Action =
// | { type: 'ADD_ITEM'; payload: { id: number; name: string; price: number } }
// | { type: 'REMOVE_ITEM'; payload: number }
// | { type: 'SET_QTY'; payload: { id: number; qty: number } }
// | { type: 'CLEAR' }
// В TypeScript компилятор ЗНАЕТ какой payload у каждого type:
// case 'ADD_ITEM': action.payload.name -> OK
// case 'REMOVE_ITEM': action.payload.name -> ERROR! payload это number
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript: action.payload: { id, name, price }
return { ...state, items: [...state.items, { ...action.payload, qty: 1 }] }
case 'REMOVE_ITEM':
// TypeScript: action.payload: number (id)
return { ...state, items: state.items.filter(i => i.id !== action.payload) }
case 'SET_QTY':
// TypeScript: action.payload: { id: number, qty: number }
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id ? { ...i, qty: action.payload.qty } : i
)
}
case 'CLEAR':
// TypeScript: action.payload — не существует! Только { type: 'CLEAR' }
return { ...state, items: [] }
default:
return state
}
}
// === 3. Обобщённая List-функция ===
// TypeScript:
// function createList<T>(keyExtractor: (item: T) => string) {
// return function render(items: T[], renderItem: (item: T) => string) { ... }
// }
function createList(keyExtractor) {
return function render(items, renderItem) {
return items.map(item => ({
key: keyExtractor(item),
content: renderItem(item)
}))
}
}
// === Демонстрация ===
console.log('=== Проверка типов props ===')
try {
Button({ label: 'Сохранить', onClick: () => {} })
Button({ label: 'Удалить', onClick: () => {}, variant: 'danger' })
Button({ label: 'Ошибка', onClick: () => {}, variant: 'invalid' }) // бросит ошибку
} catch (e) {
console.log(' TypeScript поймал бы это на этапе компиляции:', e.message)
}
console.log('
=== Reducer с discriminated union ===')
let state = { items: [] }
state = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 1, name: 'Молоко', price: 89 } })
state = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 2, name: 'Хлеб', price: 45 } })
state = cartReducer(state, { type: 'SET_QTY', payload: { id: 1, qty: 3 } })
console.log(' Товаров в корзине:', state.items.length)
console.log(' Молоко qty:', state.items[0].qty) // 3
console.log('
=== Типизированный список ===')
const renderUsers = createList(user => user.id.toString())
const users = [{ id: 1, name: 'Анна' }, { id: 2, name: 'Борис' }]
const rendered = renderUsers(users, user => `Привет, ${user.name}!`)
rendered.forEach(({ key, content }) => console.log(` [${key}] ${content}`))Вы уже знаете TypeScript. В React он особенно ценен: компоненты принимают пропсы, возвращают JSX, работают с событиями — всё это можно строго типизировать. Результат: автодополнение в IDE, ошибки на этапе компиляции, самодокументирующийся код.
Предпочтительный подход — функция с явной типизацией пропсов:
// Объявляем интерфейс для пропсов
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary' | 'danger' // опциональный с union type
disabled?: boolean
children?: React.ReactNode // принимает любой JSX
}
// Явная типизация — предпочтительный стиль
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} onClick={onClick} disabled={disabled}>
{label}
</button>
)
}
// React.FC — устаревший стиль (не рекомендуется в React 18+):
// неявно добавлял children (исправлено), хуже с generics
const Button2: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>function Form() {
// Тип события: React.ChangeEvent<HTMLInputElement>
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value) // строка
console.log(e.target.checked) // boolean (для checkbox)
console.log(e.target.files) // FileList | null (для file input)
}
// Тип для форм: React.FormEvent<HTMLFormElement>
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
// Тип для кнопок: React.MouseEvent<HTMLButtonElement>
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log(e.currentTarget.dataset.id)
}
// Тип для клавиатуры: React.KeyboardEvent<HTMLInputElement>
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submitForm()
}
}// useState: тип выводится из initialValue или явный generic
const [name, setName] = useState('') // string (вывод)
const [count, setCount] = useState(0) // number (вывод)
const [user, setUser] = useState<User | null>(null) // явный тип необходим!
// useRef: тип DOM-элемента
const inputRef = useRef<HTMLInputElement>(null)
// ref.current может быть null до монтирования — нужна проверка
inputRef.current?.focus()
// useRef для мутабельного значения (не DOM)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
// useReducer: типизация action через discriminated union
type Action =
| { type: 'increment' }
| { type: 'add'; payload: number }
| { type: 'reset'; payload: number }
interface State { count: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'add': return { count: state.count + action.payload }
case 'reset': return { count: action.payload }
}
}interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
// null как дефолт — защита от использования вне провайдера
const ThemeContext = createContext<ThemeContextType | null>(null)
// Кастомный хук с защитой
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme должен использоваться внутри ThemeProvider')
return ctx
}// Компонент, работающий с любым типом данных
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
keyExtractor: (item: T) => string | number
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
)
}
// Использование — TypeScript выводит тип T автоматически
<List
items={users}
keyExtractor={(u) => u.id} // TypeScript знает: u — User
renderItem={(u) => u.name}
/>interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
// forwardRef<ТипЭлемента, ТипПропсов>
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)// Вместо enum — as const (легче, лучше совместимость)
const TODO_ACTIONS = {
ADD: 'ADD_TODO',
TOGGLE: 'TOGGLE_TODO',
DELETE: 'DELETE_TODO',
} as const
type TodoActionType = typeof TODO_ACTIONS[keyof typeof TODO_ACTIONS]
// 'ADD_TODO' | 'TOGGLE_TODO' | 'DELETE_TODO'Типизированные паттерны React на TypeScript: props interface, события, discriminated union для reducer, generic компоненты
// Этот файл демонстрирует TypeScript-паттерны для React.
// Запустить в браузере нельзя, но код показывает реальные типы.
// === 1. Типизация компонента с пропсами ===
// interface ButtonProps {
// label: string
// onClick: () => void
// variant?: 'primary' | 'secondary' | 'danger'
// icon?: React.ReactNode
// }
//
// function Button({ label, onClick, variant = 'primary', icon }: ButtonProps) {
// return (
// <button className={`btn-${variant}`} onClick={onClick}>
// {icon && <span className="icon">{icon}</span>}
// {label}
// </button>
// )
// }
// Эмулируем проверку типов в чистом JS:
function createTypeSafeComponent(defaultProps) {
return function render(props) {
const merged = { ...defaultProps, ...props }
// Валидация типов в runtime (что делает TypeScript на этапе компиляции)
const required = ['label', 'onClick']
required.forEach(key => {
if (!(key in merged)) throw new Error(`Prop "${key}" обязателен!`)
})
const variantOptions = ['primary', 'secondary', 'danger']
if (merged.variant && !variantOptions.includes(merged.variant)) {
throw new Error(`variant должен быть одним из: ${variantOptions.join(', ')}`)
}
console.log(' Рендер Button:', merged.label, '| variant:', merged.variant)
return merged
}
}
const Button = createTypeSafeComponent({ variant: 'primary' })
// === 2. Discriminated Union для reducer ===
// TypeScript:
// type Action =
// | { type: 'ADD_ITEM'; payload: { id: number; name: string; price: number } }
// | { type: 'REMOVE_ITEM'; payload: number }
// | { type: 'SET_QTY'; payload: { id: number; qty: number } }
// | { type: 'CLEAR' }
// В TypeScript компилятор ЗНАЕТ какой payload у каждого type:
// case 'ADD_ITEM': action.payload.name -> OK
// case 'REMOVE_ITEM': action.payload.name -> ERROR! payload это number
function cartReducer(state, action) {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript: action.payload: { id, name, price }
return { ...state, items: [...state.items, { ...action.payload, qty: 1 }] }
case 'REMOVE_ITEM':
// TypeScript: action.payload: number (id)
return { ...state, items: state.items.filter(i => i.id !== action.payload) }
case 'SET_QTY':
// TypeScript: action.payload: { id: number, qty: number }
return {
...state,
items: state.items.map(i =>
i.id === action.payload.id ? { ...i, qty: action.payload.qty } : i
)
}
case 'CLEAR':
// TypeScript: action.payload — не существует! Только { type: 'CLEAR' }
return { ...state, items: [] }
default:
return state
}
}
// === 3. Обобщённая List-функция ===
// TypeScript:
// function createList<T>(keyExtractor: (item: T) => string) {
// return function render(items: T[], renderItem: (item: T) => string) { ... }
// }
function createList(keyExtractor) {
return function render(items, renderItem) {
return items.map(item => ({
key: keyExtractor(item),
content: renderItem(item)
}))
}
}
// === Демонстрация ===
console.log('=== Проверка типов props ===')
try {
Button({ label: 'Сохранить', onClick: () => {} })
Button({ label: 'Удалить', onClick: () => {}, variant: 'danger' })
Button({ label: 'Ошибка', onClick: () => {}, variant: 'invalid' }) // бросит ошибку
} catch (e) {
console.log(' TypeScript поймал бы это на этапе компиляции:', e.message)
}
console.log('
=== Reducer с discriminated union ===')
let state = { items: [] }
state = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 1, name: 'Молоко', price: 89 } })
state = cartReducer(state, { type: 'ADD_ITEM', payload: { id: 2, name: 'Хлеб', price: 45 } })
state = cartReducer(state, { type: 'SET_QTY', payload: { id: 1, qty: 3 } })
console.log(' Товаров в корзине:', state.items.length)
console.log(' Молоко qty:', state.items[0].qty) // 3
console.log('
=== Типизированный список ===')
const renderUsers = createList(user => user.id.toString())
const users = [{ id: 1, name: 'Анна' }, { id: 2, name: 'Борис' }]
const rendered = renderUsers(users, user => `Привет, ${user.name}!`)
rendered.forEach(({ key, content }) => console.log(` [${key}] ${content}`))Создай компонент ProfileCard принимающий пропсы с явными типами в JSDoc-комментариях (или просто в деструктуризации). Пропсы: name (строка), age (число), role ("admin" | "user" | "moderator"), isActive (булево). Компонент показывает все данные и окрашивает бейдж роли в разные цвета: admin — красный, moderator — оранжевый, user — синий.
roleColors[role] берёт цвет из объекта по ключу. opacity: isActive ? 1 : 0.5 делает неактивный профиль прозрачным. Дмитрий неактивен — передай isActive={false}. Используй {name}, {age}, {role} в разметке.