TypeScript в React начинает приносить пользу не тогда, когда мы красиво подписали каждую переменную, а когда компонент перестает принимать что попало. Родитель передал не тот prop, в состоянии лежит не тот тип, обработчик события читает не то поле — TypeScript ловит такие вещи раньше, чем браузер покажет пустой экран
В этом уроке соберем маленький компонент карточки урока: передадим props, добавим useState, кнопку, поле ввода и тип события для onChange. Без Redux, роутинга и архитектурной тяжести. Только тот минимум, который нужен почти в каждом React-проекте на TypeScript
- Что получится в конце
- Что нужно заранее
- Описываем данные урока
- Первый компонент
- Используем компонент в App
- Добавляем поле ввода
- Когда useState требует явный тип
- Props нельзя менять напрямую
- Частые ошибки
- Мини-задание
- Что может быть еще интересно по этой теме
- Нужно ли писать React.FC?
- Почему TypeScript не видит тип события?
- Можно ли не типизировать useState?
- Что типизировать первым в React-проекте?
- Что почитать дальше по TypeScript
Что получится в конце
Компонент будет показывать урок, отмечать его как пройденный и позволять изменить заметку:
type Lesson = {
id: number;
title: string;
minutes: number;
};
type LessonCardProps = {
lesson: Lesson;
onComplete: (id: number) => void;
};
Внутри будет useState:
const [note, setNote] = useState("");
const [isDone, setIsDone] = useState(false);
И обработчик input:
function handleNoteChange(event: React.ChangeEvent<HTMLInputElement>) {
setNote(event.target.value);
}
Что нужно заранее
У тебя уже должен быть React-проект с TypeScript. Если создаешь новый проект через Vite, обычно выбирают шаблон React + TypeScript. В готовом React-проекте нужны типы React:
npm install -D @types/react @types/react-dom
Во многих современных шаблонах они уже стоят. Проверь package.json, прежде чем ставить повторно
Описываем данные урока
Создай тип Lesson:
type Lesson = {
id: number;
title: string;
minutes: number;
};
Это не компонент. Это форма данных, которые компонент ожидает получить. Если завтра кто-то передаст minute вместо minutes, TypeScript подсветит ошибку
Теперь опишем props компонента:
type LessonCardProps = {
lesson: Lesson;
onComplete: (id: number) => void;
};
onComplete — функция, которую родитель передаст в карточку. Карточка не знает, что именно родитель сделает: отправит запрос, обновит список или покажет сообщение. Карточка знает только контракт: вызвать функцию с id урока
Первый компонент
Создай LessonCard.tsx:
import { useState } from "react";
type Lesson = {
id: number;
title: string;
minutes: number;
};
type LessonCardProps = {
lesson: Lesson;
onComplete: (id: number) => void;
};
export function LessonCard({ lesson, onComplete }: LessonCardProps) {
const [isDone, setIsDone] = useState(false);
function handleComplete() {
setIsDone(true);
onComplete(lesson.id);
}
return (
<article>
<h2>{lesson.title}</h2>
<p>Длительность: {lesson.minutes} минут</p>
<p>Статус: {isDone ? "пройден" : "в процессе"}</p>
<button onClick={handleComplete}>Отметить пройденным</button>
</article>
);
}
Здесь TypeScript проверяет сразу несколько вещей:
- у
lessonестьid,title,minutes; onCompleteможно вызвать только с числом;isDone— boolean, потому что начальное значениеfalse;- обработчик
onClickполучает функцию, а не результат вызова.
В React важно передавать обработчик так:
<button onClick={handleComplete}>
А не так:
<button onClick={handleComplete()}>
Второй вариант вызовет функцию во время рендера, а не по клику
Используем компонент в App
В App.tsx:
import { LessonCard } from "./LessonCard";
const lesson = {
id: 1,
title: "TypeScript в React",
minutes: 35,
};
export default function App() {
function handleComplete(id: number) {
console.log(`Урок ${id} пройден`);
}
return <LessonCard lesson={lesson} onComplete={handleComplete} />;
}
Теперь специально сломай объект:
const lesson = {
id: 1,
title: "TypeScript в React",
minute: 35,
};
TypeScript должен подсветить, что minutes отсутствует. Это мелочь, которая в JavaScript легко превращается в undefined на странице
Добавляем поле ввода
Теперь добавим заметку:
export function LessonCard({ lesson, onComplete }: LessonCardProps) {
const [isDone, setIsDone] = useState(false);
const [note, setNote] = useState("");
function handleComplete() {
setIsDone(true);
onComplete(lesson.id);
}
function handleNoteChange(event: React.ChangeEvent<HTMLInputElement>) {
setNote(event.target.value);
}
return (
<article>
<h2>{lesson.title}</h2>
<p>Длительность: {lesson.minutes} минут</p>
<p>Статус: {isDone ? "пройден" : "в процессе"}</p>
<label>
Заметка:
<input value={note} onChange={handleNoteChange} />
</label>
<p>Текущая заметка: {note || "пока пусто"}</p>
<button onClick={handleComplete}>Отметить пройденным</button>
</article>
);
}
Тип React.ChangeEvent<HTMLInputElement> говорит TypeScript, что событие пришло от input. Поэтому event.target.value понятен и безопасен
Если обработчик написать inline, тип часто выводится сам:
<input value={note} onChange={(event) => setNote(event.target.value)} />
Но для отдельной функции явный тип события обычно помогает
Когда useState требует явный тип
В простых случаях TypeScript выводит тип из начального значения:
const [note, setNote] = useState("");
const [isDone, setIsDone] = useState(false);
Но если начальное значение null, лучше подсказать:
type User = {
id: number;
name: string;
};
const [user, setUser] = useState<User | null>(null);
Иначе TypeScript может решить, что состояние всегда null, или придется потом спорить с типами
Props нельзя менять напрямую
Ошибка новичка:
lesson.minutes = lesson.minutes + 5;
В React props нужно воспринимать как входные данные. Если нужно изменить состояние, делай это в родителе или через useState. TypeScript может помочь заметить часть таких ошибок, но главное правило здесь реактовое: не мутировать props
Частые ошибки
Первая ошибка — писать props как any, чтобы "быстрее". Компонент начнет принимать все, и TypeScript не поймает опечатки
Вторая ошибка — вызывать обработчик в JSX: onClick={handleComplete()} вместо onClick={handleComplete}
Третья ошибка — не типизировать состояние с null. Если значение сначала пустое, но потом станет объектом, лучше явно написать User | null
Четвертая ошибка — путать props и state. Props приходят от родителя, state живет внутри компонента и меняется через setter
Мини-задание
Добавь prop difficulty:
type Difficulty = "easy" | "medium" | "hard";
type Lesson = {
id: number;
title: string;
minutes: number;
difficulty: Difficulty;
};
Выведи сложность в карточке. Потом попробуй передать:
difficulty: "normal"
TypeScript должен подсветить ошибку, потому что такого значения нет в union
Что может быть еще интересно по этой теме
Нужно ли писать React.FC?
Не обязательно. Многие современные примеры просто типизируют props и пишут обычную функцию компонента. Главное, чтобы props были описаны явно
Почему TypeScript не видит тип события?
В inline-обработчиках тип часто выводится автоматически. Если выносишь функцию отдельно, укажи React.ChangeEvent<HTMLInputElement> или другой подходящий тип
Можно ли не типизировать useState?
Можно, если начальное значение понятное: строка, число, boolean. Если начальное значение null или пустой массив, явный тип часто нужен
Что типизировать первым в React-проекте?
Начни с props компонентов и данных, которые приходят из API. Это быстро дает пользу и не перегружает проект
Что почитать дальше по TypeScript
- interface и type в TypeScript: когда что использовать — база для описания props.
- Union, literal types и enum в TypeScript — удобно для режимов, статусов и вариантов UI.
- Generics в TypeScript: первый пример без головоломок — пригодится для переиспользуемых компонентов.
- tsconfig.json простыми словами: strict, target и module — чтобы понимать настройки React-проекта.



