TypeScript в React: props, state и события

TypeScript в React начинает приносить пользу не тогда, когда мы красиво подписали каждую переменную, а когда компонент перестает принимать что попало. Родитель передал не тот prop, в состоянии лежит не тот тип, обработчик события читает не то поле — TypeScript ловит такие вещи раньше, чем браузер покажет пустой экран

В этом уроке соберем маленький компонент карточки урока: передадим props, добавим useState, кнопку, поле ввода и тип события для onChange. Без Redux, роутинга и архитектурной тяжести. Только тот минимум, который нужен почти в каждом 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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x