Урок 42 из 49 · Месяц 6. TypeScript и выход на работу
TypeScript: установка, type, interface и generics
Содержание урока
- Что такое TypeScript и зачем он нужен
- Установка TypeScript
- Вариант 1. Отдельно, чтобы поиграться
- Вариант 2. React-проект на Vite (как в реальной работе)
- Базовые типы
- Примитивы
- Вывод типов
- Массивы и кортежи
- Объекты
- Объединения и литеральные типы
- any и unknown
- Типизация функций
- type и interface
- type
- interface
- Расширение типов
- Что выбрать
- Generics — типы-параметры
- Проблема
- Решение
- Generic в интерфейсах
- Встроенные утилиты
- TypeScript в React
- Типизация пропсов
- children
- useState
- События
- useRef
- Типичные ошибки
- Практика
- Итоги
TypeScript — это JavaScript с «подписями»: вы заранее указываете, какие данные лежат в переменной, что принимает функция и что она возвращает. Редактор и компилятор проверяют эти подписи и находят ошибки ещё до запуска программы. В этом уроке установим TypeScript, разберём базовые типы, type, interface и generics, а в конце научимся типизировать React-компоненты и их пропсы.
Что такое TypeScript и зачем он нужен
JavaScript — язык с динамической типизацией: переменная может сегодня хранить число, а завтра строку, и никто вас не остановит. Ошибка всплывёт только тогда, когда пользователь нажмёт кнопку и увидит белый экран.
function getTotal(price, count) {
return price * count;
}
getTotal({}, 2); // NaN — ошибка тихо ушла дальше
TypeScript добавляет статическую типизацию: типы проверяются заранее, во время написания кода.
function getTotal(price: number, count: number): number {
return price * count;
}
getTotal(100, 2); // ✅ 200
getTotal("100", 2); // ❌ Argument of type 'string' is not assignable to parameter of type 'number'
Аналогия: JavaScript — коробки без надписей, содержимое видно, только открыв их. TypeScript — коробки с наклейками «тут только книги»: положить туда кастрюлю не дадут.
Что даёт TypeScript на практике:
- ошибки видны в редакторе — красное подчёркивание до запуска;
- автодополнение — редактор знает, какие поля есть у объекта
user, и подсказывает их; - безопасный рефакторинг — переименовали поле, и TypeScript покажет все места, которые нужно поправить.
Установка TypeScript
Вариант 1. Отдельно, чтобы поиграться
mkdir ts-practice && cd ts-practice
npm init -y
npm install -D typescript
npx tsc --init
npm install -D typescript— ставим TypeScript как dev-зависимость (нужна только при разработке, подробнее — в уроке npm и package.json);npx tsc --init— создаёт файл настроекtsconfig.json;tsc— это TypeScript Compiler: он проверяет типы и превращает.tsв.js. Например,npx tsc index.tsсоздаст рядомindex.js, который запускается черезnode index.js.
Вариант 2. React-проект на Vite (как в реальной работе)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
В шаблоне react-ts компоненты имеют расширение .tsx (TypeScript + JSX), обычные модули — .ts, а tsconfig-файлы уже настроены.
Самая важная настройка в tsconfig.json — "strict": true: строгий режим, в котором TypeScript проверяет null/undefined и не пропускает неявный any. В шаблоне Vite он уже включён — не выключайте его, иначе большая часть ошибок пройдёт незамеченной.
Базовые типы
Примитивы
let userName: string = "Айгерим";
let age: number = 21; // и целые, и дробные числа
let isAdmin: boolean = false;
Вывод типов
Писать тип везде не обязательно. TypeScript умеет выводить тип сам — по значению:
let city = "Бишкек"; // TypeScript понял: string
city = 42; // ❌ Type 'number' is not assignable to type 'string'
Правило: если значение понятно — тип не пишем. Явно указываем типы у параметров функций, пустых значений и сложных объектов.
Массивы и кортежи
const scores: number[] = [5, 4, 5];
const tags: Array<string> = ["react", "ts"]; // то же самое, другая запись
// Кортеж (tuple) — массив фиксированной длины, где у каждой позиции свой тип
const point: [number, number] = [10, 20];
Объекты
const user: { id: number; name: string; email?: string } = { id: 1, name: "Тимур" };
Знак ? после имени поля означает необязательное поле: его может не быть, и тогда там undefined.
Объединения и литеральные типы
Union (объединение) — «или то, или другое». Пишется через |:
let id: number | string;
id = 10; // ✅
id = "abc"; // ✅
id = true; // ❌
// Литеральный тип — разрешены только конкретные значения
let status: "loading" | "success" | "error" = "loading";
status = "success"; // ✅
status = "done"; // ❌ такого варианта нет
Литеральные типы удобны для статусов, размеров кнопок, ролей — опечатка сразу подсветится.
any и unknown
any— «что угодно, не проверяй». Это фактически отключение TypeScript, избегайте его.unknown— «что угодно, но сначала проверь». Подходит для данных неизвестного вида: ответ сервера,errorвcatch.
function parse(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase(); // здесь TypeScript уже знает, что это string
}
return null;
}
Проверка typeof value === "string" называется сужением типа (narrowing): внутри if TypeScript «сужает» unknown до string.
Типизация функций
// Параметры и возвращаемое значение
function add(a: number, b: number): number {
return a + b;
}
// Стрелочная функция и необязательный параметр
const greet = (name: string, greeting?: string): string =>
`${greeting ?? "Привет"}, ${name}!`;
// Функция, которая ничего не возвращает
function logMessage(message: string): void {
console.log(message);
}
void означает «функция ничего не возвращает». Если функция асинхронная, её результат описывают как Promise<тип>, например Promise<number>.
type и interface
Чтобы переиспользовать описание объекта, есть псевдонимы типов (type) и интерфейсы (interface).
type
type User = {
id: number;
name: string;
email?: string;
role: "admin" | "user";
};
const admin: User = { id: 1, name: "Дана", role: "admin" };
type умеет давать имя чему угодно — не только объектам:
type ID = number | string;
type Status = "idle" | "loading" | "success" | "error";
type Point = [number, number];
interface
interface Product {
id: number;
title: string;
price: number;
readonly createdAt: string; // readonly — поле нельзя менять после создания: phone.createdAt = "..." даст ошибку
}
const phone: Product = {
id: 7,
title: "Смартфон",
price: 25000,
createdAt: "2026-01-15",
};
Расширение типов
Интерфейс расширяют через extends, а type — через пересечение &:
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
type Person = { name: string };
type Employee = Person & { position: string };
const dog: Dog = { name: "Рекс", breed: "овчарка" };
Что выбрать
Главные отличия: type умеет описывать не только объекты, но и union, кортежи, примитивы, а interface можно расширять через extends и «дописывать», объявив второй раз с тем же именем.
Generics — типы-параметры
Проблема
Напишем функцию, которая возвращает первый элемент массива:
function first(arr: any[]): any {
return arr[0]; // работает с любым массивом, но результат — any, подсказки пропали
}
Решение
Generic (дженерик) — это тип-параметр, «переменная для типа». Её пишут в угловых скобках, обычно буквой T:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // T = number, тип n: number | undefined
const s = first(["a", "b"]); // T = string
const u = first<string>([]); // T можно указать явно
Аналогия: generic — форма для печенья: форма одна, тесто любое.
Generic в интерфейсах
Частый пример — ответ сервера, у которого обёртка всегда одинаковая, а данные разные:
interface ApiResponse<T> {
data: T;
total: number;
page: number;
}
type Todo = { id: number; title: string; completed: boolean };
const response: ApiResponse<Todo[]> = {
data: [{ id: 1, title: "Выучить TS", completed: false }],
total: 1,
page: 1,
};
Встроенные утилиты
TypeScript содержит готовые generic-типы, которые «переделывают» другие типы:
interface User {
id: number;
name: string;
email: string;
}
type UserUpdate = Partial<User>; // все поля стали необязательными
type UserPreview = Pick<User, "id" | "name">; // только id и name
type NewUser = Omit<User, "id">; // всё, кроме id
type UsersById = Record<number, User>; // объект: ключ — число, значение — User
Omit<User, "id"> особенно полезен для форм создания: id ещё нет, его выдаст сервер.
TypeScript в React
Типизация пропсов
Пропсы описываем типом и указываем его у параметра компонента:
type ButtonProps = {
text: string;
variant?: "primary" | "secondary";
disabled?: boolean;
onClick: () => void;
};
export function Button({ text, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
{text}
</button>
);
}
Разбор:
variant?иdisabled?— необязательные, значения по умолчанию задаём при деструктуризации;onClick: () => void— функция без аргументов, которая ничего не возвращает.
Теперь при использовании редактор подскажет все пропсы и отругает за ошибки:
<Button text="Сохранить" onClick={() => console.log("save")} /> // ✅
<Button text="Удалить" variant="danger" onClick={() => {}} /> // ❌ "danger" нет в списке
<Button onClick={() => {}} /> // ❌ не передан text
children
Если компонент оборачивает другой JSX, используйте тип React.ReactNode — он принимает всё, что можно отрендерить: текст, числа, элементы, массивы, null.
import type { ReactNode } from "react";
type CardProps = {
title: string;
children: ReactNode;
};
export function Card({ title, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
{children}
</div>
);
}
useState
Обычно тип выводится из начального значения. Указывать его явно нужно, когда начальное значение «пустое»:
const [count, setCount] = useState(0); // number — выведено
const [user, setUser] = useState<User | null>(null); // пока null, потом User
const [todos, setTodos] = useState<Todo[]>([]); // без <Todo[]> был бы never[]
Пока user может быть null, TypeScript не даст обратиться к user.name без проверки — пишите user?.name ?? "Гость".
События
import { useState, type ChangeEvent, type FormEvent } from "react";
export function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {
const [query, setQuery] = useState("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={handleChange} />
<button type="submit">Найти</button>
</form>
);
}
ChangeEvent<HTMLInputElement>— событие изменения именно у<input>, поэтомуe.target.valueизвестен;FormEvent<HTMLFormElement>— событие отправки формы.
useRef
Для ссылки на DOM-элемент указываем его тип и начальное null: const inputRef = useRef<HTMLInputElement>(null). Обращаемся через inputRef.current?.focus() — пока элемент не отрисован, current равен null.
Типичные ошибки
- Ставить
any, чтобы «ошибка ушла». Ошибка не уходит — вы просто перестаёте её видеть. Используйтеunknownи проверки. - Обращаться к полю, которое может быть
undefined. ОшибкаObject is possibly 'undefined'— это подсказка: добавьте проверку или?.. - Пустой массив без типа:
useState([])даётnever[], и в него ничего нельзя положить. ПишитеuseState<Todo[]>([]). - Путать
asс проверкой.data as User— это не проверка, а «поверь мне». Используйте его осторожно.
Официальная документация с примерами: typescriptlang.org/docs и раздел про TypeScript на react.dev.
Практика
- Типы для магазина. Опишите
interface Product(id, title, price, необязательное поле discount) иtype Category = "phones" | "laptops" | "audio". Создайте массив из трёх товаров. Ожидаемый результат: товар безpriceили с категорией"tv"подсвечивается ошибкой. - Generic. Напишите
function last<T>(arr: T[]): T | undefinedиfunction groupBy<T>(items: T[], getKey: (item: T) => string): Record<string, T[]>. Ожидаемый результат:last(["a", "b"])имеет типstring | undefined. - React-компонент. Создайте проект
react-tsна Vite и компонентProductCardс пропсамиproduct: ProductиonBuy: (id: number) => void. Ожидаемый результат: без переданногоonBuyпроект не собирается командойnpm run build. - Перевод проекта. Возьмите свой ToDoList из урока ToDoList: axios и json-server и переведите на TypeScript: тип
Todo, типизированныйuseState, обработчики событий. Ожидаемый результат:npx tsc -bпроходит без ошибок,anyв коде нет.
Итоги
- TypeScript — это JavaScript с типами; типы проверяются до запуска и удаляются при сборке.
- React-проект на TS создаётся командой
npm create vite@latest my-app -- --template react-ts. - Базовые типы:
string,number,boolean, массивы, кортежи, union|, литеральные типы; вместоany—unknown. typeиinterfaceдают имя форме данных;interfaceрасширяется черезextends,type— через&.- Generics (
<T>) позволяют писать одну функцию или тип для разных данных без потери подсказок. - В React типизируем пропсы,
children: ReactNode,useState<T>, события (ChangeEvent,FormEvent) иuseRef<HTMLElement>(null).