В маленьком демо React без типов может казаться быстрее, но в реальном проекте без TypeScript начинают накапливаться «тихие» баги: компонент получает не те props, API вернуло неожиданный формат, событие обработано не тем типом элемента. TypeScript решает это на этапе разработки.
Основная идея: типизировать границы системы. В React это прежде всего props компонентов, результаты API, состояние и обработчики событий. Когда эти точки покрыты типами, большая часть ошибок ловится до запуска приложения.
Не нужно сразу пытаться типизировать всё на 100%. Начни с базового минимума:
Этого достаточно, чтобы код стал заметно стабильнее и проще в поддержке.
Предпочтительный способ — функция с явным типом props (без React.FC):
// Рекомендуемый способ — явная типизация props:
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
children: React.ReactNode
}
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
)
}
// React.FC<Props> — устаревший стиль, имеет нюансы:
// - неявно добавлял children (исправлено в React 18)
// - несколько хуже работает с generics
const Button2: React.FC<ButtonProps> = ({ label, onClick }) => { ... }interface CardProps {
title: string
children: React.ReactNode // строка, JSX, массив, null — всё подходит
}
// Альтернативы:
// React.ReactElement — только JSX элементы
// JSX.Element — синоним ReactElement
// string — только строкиfunction Form() {
// Кнопка
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log('clicked')
}
// Инпут
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
// Форма
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
)
}// useState — тип выводится из начального значения
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)
// inputRef.current может быть null до монтирования
// useRef — изменяемое значение (не DOM)
const timerRef = useRef<number | null>(null)
timerRef.current = setTimeout(() => {}, 1000)
// useReducer с типами
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { 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 'decrement': return { count: state.count - 1 }
case 'reset': return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'reset', payload: 10 }) // payload обязателен!function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T) => {
setStored(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [stored, setValue]
}
// Использование:
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)1. Слишком общий тип `any` в начале проекта. Он временно «разблокирует» код, но быстро убивает пользу от TypeScript.
2. Игнорирование `null` в `useRef` и `useContext`. В React это один из самых частых источников runtime-ошибок.
3. Смешивание доменной модели и UI-типов. Лучше разделять типы API-данных и типы состояния конкретного компонента.
React-like система в чистом JS: createElement, useState хук и минимальный рендер — демонстрация как хуки работают внутри
// Реализуем упрощённый React в чистом JS — чтобы понять
// как работают хуки и почему TypeScript так важен в React.
// --- Минимальный React-like движок ---
let currentComponent = null
let hookIndex = 0
const componentStates = new Map()
function useState(initialValue) {
const component = currentComponent
const index = hookIndex++
if (!componentStates.has(component)) {
componentStates.set(component, [])
}
const states = componentStates.get(component)
if (states[index] === undefined) {
states[index] = initialValue
}
const setState = (newValue) => {
states[index] = typeof newValue === 'function'
? newValue(states[index])
: newValue
// В реальном React здесь был бы re-render
console.log(` [setState] ${component}.hook[${index}] = ${JSON.stringify(states[index])}`)
}
return [states[index], setState]
}
function runComponent(name, componentFn, props = {}) {
currentComponent = name
hookIndex = 0
console.log(`\n--- Рендер компонента: ${name} ---`)
const result = componentFn(props)
currentComponent = null
return result
}
// --- Компоненты ---
function Counter({ initialCount = 0 }) {
// TypeScript: useState<number>(initialCount)
const [count, setCount] = useState(initialCount)
// TypeScript: onClick: React.MouseEventHandler
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const reset = () => setCount(initialCount)
console.log(` count = ${count}`)
return { count, increment, decrement, reset }
}
function Form() {
// TypeScript: useState<string>('')
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [errors, setErrors] = useState({})
// TypeScript: (e: React.ChangeEvent<HTMLInputElement>) => void
const handleNameChange = (value) => setName(value)
const handleEmailChange = (value) => setEmail(value)
const validate = () => {
const newErrors = {}
if (!name) newErrors.name = 'Имя обязательно'
if (!email.includes('@')) newErrors.email = 'Некорректный email'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
console.log(` name="${name}", email="${email}"`)
return { name, email, errors, handleNameChange, handleEmailChange, validate }
}
// --- Демонстрация ---
console.log('=== Counter ===')
const counter = runComponent('Counter', Counter, { initialCount: 5 })
counter.increment()
counter.increment()
counter.decrement()
// Второй рендер — состояние сохранено
const counter2 = runComponent('Counter', Counter, { initialCount: 5 })
console.log('После двух increment и одного decrement, count =', componentStates.get('Counter')[0])
console.log('\n=== Form ===')
const form = runComponent('Form', Form)
form.handleNameChange('Алексей')
form.handleEmailChange('alex')
const form2 = runComponent('Form', Form)
console.log('Валидация (email без @):', form2.validate())
form.handleEmailChange('alex@example.com')
const form3 = runComponent('Form', Form)
console.log('Валидация (корректный email):', form3.validate())В маленьком демо React без типов может казаться быстрее, но в реальном проекте без TypeScript начинают накапливаться «тихие» баги: компонент получает не те props, API вернуло неожиданный формат, событие обработано не тем типом элемента. TypeScript решает это на этапе разработки.
Основная идея: типизировать границы системы. В React это прежде всего props компонентов, результаты API, состояние и обработчики событий. Когда эти точки покрыты типами, большая часть ошибок ловится до запуска приложения.
Не нужно сразу пытаться типизировать всё на 100%. Начни с базового минимума:
Этого достаточно, чтобы код стал заметно стабильнее и проще в поддержке.
Предпочтительный способ — функция с явным типом props (без React.FC):
// Рекомендуемый способ — явная типизация props:
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
children: React.ReactNode
}
function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
)
}
// React.FC<Props> — устаревший стиль, имеет нюансы:
// - неявно добавлял children (исправлено в React 18)
// - несколько хуже работает с generics
const Button2: React.FC<ButtonProps> = ({ label, onClick }) => { ... }interface CardProps {
title: string
children: React.ReactNode // строка, JSX, массив, null — всё подходит
}
// Альтернативы:
// React.ReactElement — только JSX элементы
// JSX.Element — синоним ReactElement
// string — только строкиfunction Form() {
// Кнопка
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
console.log('clicked')
}
// Инпут
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
// Форма
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
)
}// useState — тип выводится из начального значения
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)
// inputRef.current может быть null до монтирования
// useRef — изменяемое значение (не DOM)
const timerRef = useRef<number | null>(null)
timerRef.current = setTimeout(() => {}, 1000)
// useReducer с типами
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { 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 'decrement': return { count: state.count - 1 }
case 'reset': return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
dispatch({ type: 'reset', payload: 10 }) // payload обязателен!function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T) => {
setStored(value)
localStorage.setItem(key, JSON.stringify(value))
}
return [stored, setValue]
}
// Использование:
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light')interface ThemeContextType {
theme: 'light' | 'dark'
toggle: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
function useTheme(): ThemeContextType {
const ctx = useContext(ThemeContext)
if (!ctx) throw new Error('useTheme must be used within ThemeProvider')
return ctx
}interface InputProps {
placeholder?: string
value: string
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, value, onChange }, ref) => (
<input ref={ref} placeholder={placeholder} value={value} onChange={onChange} />
)
)1. Слишком общий тип `any` в начале проекта. Он временно «разблокирует» код, но быстро убивает пользу от TypeScript.
2. Игнорирование `null` в `useRef` и `useContext`. В React это один из самых частых источников runtime-ошибок.
3. Смешивание доменной модели и UI-типов. Лучше разделять типы API-данных и типы состояния конкретного компонента.
React-like система в чистом JS: createElement, useState хук и минимальный рендер — демонстрация как хуки работают внутри
// Реализуем упрощённый React в чистом JS — чтобы понять
// как работают хуки и почему TypeScript так важен в React.
// --- Минимальный React-like движок ---
let currentComponent = null
let hookIndex = 0
const componentStates = new Map()
function useState(initialValue) {
const component = currentComponent
const index = hookIndex++
if (!componentStates.has(component)) {
componentStates.set(component, [])
}
const states = componentStates.get(component)
if (states[index] === undefined) {
states[index] = initialValue
}
const setState = (newValue) => {
states[index] = typeof newValue === 'function'
? newValue(states[index])
: newValue
// В реальном React здесь был бы re-render
console.log(` [setState] ${component}.hook[${index}] = ${JSON.stringify(states[index])}`)
}
return [states[index], setState]
}
function runComponent(name, componentFn, props = {}) {
currentComponent = name
hookIndex = 0
console.log(`\n--- Рендер компонента: ${name} ---`)
const result = componentFn(props)
currentComponent = null
return result
}
// --- Компоненты ---
function Counter({ initialCount = 0 }) {
// TypeScript: useState<number>(initialCount)
const [count, setCount] = useState(initialCount)
// TypeScript: onClick: React.MouseEventHandler
const increment = () => setCount(c => c + 1)
const decrement = () => setCount(c => c - 1)
const reset = () => setCount(initialCount)
console.log(` count = ${count}`)
return { count, increment, decrement, reset }
}
function Form() {
// TypeScript: useState<string>('')
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [errors, setErrors] = useState({})
// TypeScript: (e: React.ChangeEvent<HTMLInputElement>) => void
const handleNameChange = (value) => setName(value)
const handleEmailChange = (value) => setEmail(value)
const validate = () => {
const newErrors = {}
if (!name) newErrors.name = 'Имя обязательно'
if (!email.includes('@')) newErrors.email = 'Некорректный email'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
console.log(` name="${name}", email="${email}"`)
return { name, email, errors, handleNameChange, handleEmailChange, validate }
}
// --- Демонстрация ---
console.log('=== Counter ===')
const counter = runComponent('Counter', Counter, { initialCount: 5 })
counter.increment()
counter.increment()
counter.decrement()
// Второй рендер — состояние сохранено
const counter2 = runComponent('Counter', Counter, { initialCount: 5 })
console.log('После двух increment и одного decrement, count =', componentStates.get('Counter')[0])
console.log('\n=== Form ===')
const form = runComponent('Form', Form)
form.handleNameChange('Алексей')
form.handleEmailChange('alex')
const form2 = runComponent('Form', Form)
console.log('Валидация (email без @):', form2.validate())
form.handleEmailChange('alex@example.com')
const form3 = runComponent('Form', Form)
console.log('Валидация (корректный email):', form3.validate())Реализуй типизированный `createEmitter<T>()` — EventEmitter с дженериком для типа данных события. Интерфейс `Emitter<T>`: методы `on(event: string, handler: (data: T) => void): void`, `emit(event: string, data: T): void`, `off(event: string, handler: (data: T) => void): void`. Используй `Map<string, Set<(data: T) => void>>` для хранения обработчиков.
Объяви `const listeners: Map<string, Set<EventHandler<T>>> = new Map()`. В `on()`: если нет записи — `listeners.set(event, new Set())`, затем `listeners.get(event)!.add(handler)`. В `off()`: `listeners.get(event)?.delete(handler)`. В `emit()`: `listeners.get(event)?.forEach(h => h(data))`.