TypeScript — JavaScript с типами
TypeScript — это надмножество JavaScript: весь валидный JS автоматически валиден и в TS. Главное отличие — статические типы, которые проверяются при компиляции. Ошибка «undefined is not a function» ловится ещё до запуска, а IDE получает полный автокомплит.
1. Что такое TypeScript
TypeScript разработан Microsoft. Код с расширением .ts компилируется в обычный JavaScript, который понимают все браузеры и Node.js. Времени выполнения у типов нет — они существуют только на этапе компиляции.
- ранняя поимка ошибок — до запуска;
- самодокументирование: типы открывают контракт функции;
- умные подсказки в VS Code для любого API;
- рефакторинг без страха сломать вызовы.
2. Установка и компиляция
npm init -y
npm install --save-dev typescript
npx tsc --init # создаст tsconfig.json
npx tsc app.ts # скомпилировать один файл// app.ts
let name: string = "Анна";
name.toUpperCase(); // ок
name = 42; // ошибка: число нельзя в переменную stringnoEmitOnError: true.3. tsconfig.json
Файл настройки компилятора. Важнейшие опции:
| Опция | Значение |
|---|---|
target | версия ECMAScript на выходе (ES2020, ES2022) |
module | система модулей: commonjs, esnext |
strict | включить все strict-проверки (рекомендуется true) |
outDir | куда класть скомпилированные .js |
include | какие файлы компилировать |
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"outDir": "./dist",
"esModuleInterop": true
},
"include": ["src"]
}"strict": false «чтобы не ругалось». Это плохая привычка: включай strict сразу, ошибки стоят того.4. Базовые типы
let isDone: boolean = false;
let age: number = 30;
let userName: string = "Пётр";
let list: number[] = [1, 2, 3];
let pair: [string, number] = ["имя", 42]; // кортеж (tuple)
let maybe: string | null = null; // union
let anything: unknown = 42; // неизвестный тип
let anything2: any = { a: 1 }; // отключение проверок (осторожно)Также есть void (функция ничего не возвращает), never (никогда не завершится выбросом/return) и литеральные типы:
type Size = "sm" | "md" | "lg";
let btn: Size = "md"; // только эти три строки5. Функции
Типы параметров и возвращаемого значения:
function add(a: number, b: number): number {
return a + b;
}
const greet = (name: string): string => `Привет, ${name}!`;
function log(msg: string): void {
console.log(msg);
}Необязательные параметры — через ?, значения по умолчанию — как в JS:
function format(price: number, currency = "₽"): string {
return `${price} ${currency}`;
}6. Интерфейсы и псевдонимы типов
interface описывает форму объекта, type создаёт псевдоним любого типа:
interface User {
id: number;
name: string;
email?: string; // необязательное поле
role: "admin" | "user";
}
type Point = { x: number; y: number };
function printUser(u: User): void {
console.log(u.name);
}Интерфейсы можно расширять:
interface Admin extends User {
permissions: string[];
}interface — для объектов и расширяемых контрактов, type — для union, кортежей и примитивов.7. Классы и модификаторы
class Account {
private balance: number = 0; // доступ только внутри класса
protected owner: string; // внутри класса и наследников
public readonly id: string; // только чтение
constructor(owner: string) {
this.owner = owner;
this.id = crypto.randomUUID();
}
deposit(amount: number): void {
this.balance += amount;
}
}Краткая запись параметров-свойств:
class Point {
constructor(private x: number, private y: number) {}
}8. Дженерики
Дженерики позволяют функции работать с любым типом, сохраняя проверку типов:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
interface Pair<K, V> {
key: K;
value: V;
}Условия на дженерик (generic constraints):
function getLength<T extends { length: number }>(obj: T): number {
return obj.length;
}
getLength("text"); // ок: строка имеет length9. DOM и strict null checks
C strict: true включён strictNullChecks — переменные-ссылки не считаются автоматически ненулевыми:
const btn = document.getElementById("btn");
btn.addEventListener("click", handler);
// ошибка: btn может быть null. Решения:
btn?.addEventListener("click", handler); // опциональная цепочка
if (btn) {
btn.addEventListener("click", handler); // сужение типа
}Когда уверен, что элемент точно есть, используй ! (non-null assertion):
const input = document.querySelector("input")!;! — это «доверяю себе». Если элемент вдруг удалён, ошибка упадёт в рантайме, поэтому применяй оператор осознанно.10. TypeScript в проектах
В современных проектах TS настраивают через инструменты сборки, а не вручную:
# Vite: шаблон с TypeScript
npm create vite@latest my-app -- --template vue-ts
npm create vite@latest my-app -- --template react-ts- во Vue типы влетают в props/emits/slots автоматически (см. курс Vue 3);
- в React типизируются props через
interface Props(курс React); - тесты на Vitest и Jest тоже пишут на TS;
- API-запросы типизируются моделями ответов (курс REST API).
// Пример модели ответа API
interface Product {
id: number;
title: string;
price: number;
}
fetch("/api/products")
.then(r => r.json() as Promise<Product[]>)
.then(products => products.map(p => p.title));11. Сужение типов и discriminated union
Операторы typeof, in и instanceof позволяют сузить тип прямо в ветке кода: внутри if TypeScript сам понимает, с каким типом работает.
type Result = { ok: true; data: string }
| { ok: false; error: string };
function handle(r: Result) {
if (r.ok) {
console.log(r.data); // data гарантировано
} else {
console.error(r.error); // error гарантировано
}
}Такой подход называется discriminated union: поле-дискриминатор (ok) различает варианты, а компилятор проверяет все ветки. Если добавить новый вариант — ошибки появятся во всех местах, где он не обработан.
never: в ветке default присвойте значение к never — компилятор укажет, что не рассмотрен вариант.any: as unknown as T допустим в редких местах, но это всегда признак, что тип можно выразить точнее.12. Декларации типов: .d.ts и @types
Пакет без своих типов приходит с файлами *.d.ts — декларациями, в которых описана только сигнатура API. Для популярных библиотек типы живут в @types/*.
npm install -D @types/expressСобственная декларация для модуля без типов создаётся в файле src/global.d.ts:
declare module "old-library" {
export function run(opts: { verbose?: boolean }): string;
}export function/export const— что экспортирует модуль;interfaceвнутри декларации — форма объектов;- глобальные переменные объявляются через
declare const.
Декларации — публичный интерфейс библиотеки: обновляйте их вместе с функцией. Проверка этих файлов включается опцией skipLibCheck: false, а опечатки в них видны уже при первом использовании в проекте.
any в декларациях «чтобы закомпилировалось»: такие типы обманывают и вас, и других разработчиков проекта.13. Поэтапный перевод проекта на TS
Переводить существующий проект на TypeScript безопаснее постепенно: включить компилятор, починить ошибки, а потом ужестить параметры.
- Установите dev-зависимости и добавьте
tsconfig.jsonсallowJs: trueиcheckJs: false. - Переименуйте файлы в
.ts.tsxпо одному, начиная с новых модулей. - Опишите модели данных (API-ответы, состояние) — они дают наибольшую пользу.
- Включите
strict: false, затем по частям поднимайте флаг до полного strict.
// Начальный tsconfig для миграции JS → TS
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"strict": true
}
}Строгий режим включайте в конце: сначала типобезопасность на новых файлах, затем разбор «тёмных углов» старого кода.
14. Utility types и практика их применения
Utility types — готовые обёртки над типами: вы не пишете каждое преобразование руками, а выражаете намерение компактно и без ошибок.
interface User {
id: number;
name: string;
email: string;
}
type CreateUser = Omit<User, "id">; // без id — для формы создания
type PublicProfile = Pick<User, "id" | "name">;
type PatchUser = Partial<User>; // любое поле опционально
type ReadonlyUser = Readonly<User>;
type Email = string | undefined;
type RequiredEmail = NonNullable<Email>;Omit/Pickсобирают подмножества полей;Partial/Requiredменяют опциональность;Record<K, V>строит словарь,ReturnTypeвытаскивает тип результата функции.
Omit<T, K> внутри Pick и наоборот — на десяти строках получаются аккуратные доменные типы.15. TypeScript на Node.js и валидация (Zod)
TypeScript силён и на сервере: типы описывают API, а инструменты валидации превращают декларацию в рантайм-проверку — данные снаружи нельзя гарантировать типом.
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email()
});
type CreateUserDto = z.infer<typeof CreateUserSchema>;- схема zod автоматически выводит тип — один источник истины;
- валидация входа и тела запроса исключает «типовые» баги на границе системы;
- на Node.js используйте
tsxили ts-node для разработки и tsc для сборки.
16. Производительность компиляции
Большой проект на TS начинает компилироваться медленно. Основные рычаги — точечность проверок, кэш и организация проекта, а не «выключение strict».
// tsconfig.json
{
"compilerOptions": {
"incremental": true,
"skipLibCheck": true,
"noEmit": true
}
}incrementalкэширует прошлую компиляцию — повторные запуски быстрее;skipLibCheckпропускает проверку d.ts других пакетов;Project Referencesделят проект на части и компилируют только изменённые.
Куда проседают секунды: лишние include-папки, тяжёлые d.ts в node_modules, «любые» типы, заставляющие компилятор догадываться. Следите за временем одного tsc --noEmit в CI и не позволяйте ему расти незаметно.
--extendedDiagnostics: он покажет, где именно тратится время.incremental разовый запуск обычно быстрее, чем выключение проверок вручную. Точность важнее секунд на старте.17. Общие типы фронта и бэка
Самая высокая цена типа — когда он один на клиент и сервер: единожды описанный контракт API не разъезжается при каждой правке.
- OpenAPI: опишите API один раз, сгенерируйте типы для TS-клиента и сервера;
- graphql-codegen: типы вырастают прямо из схемы GraphQL;
- Общий пакет: положите размеченные типы в npm-пакет
@acme/shared, чтобы обе стороны импортировали одно.
// shared/types.ts — единый источник правды
export interface Order {
id: string;
totalCents: number;
status: "pending" | "paid" | "cancelled";
}
export type CreateOrder = Omit<Order, "id" | "status">;18. Декораторы и расширения класса
Декораторы — функции-обёртки над классами, методами и свойствами. Их включают опцией experimentalDecorators в tsconfig; на практике они лежат в основе NestJS, TypeORM и аннотаций валидации.
function log(target: any, key: string) {
console.log(`Метод вызван: ${key}`)
}
class User {
@log
save() { return "ok" }
}- class-декоратор получает класс, property — прототип и имя;
- метаданные (
reflect-metadata) хранят «аннотации» для DI и валидации; - дизайн-паттерн: декоратор не меняет поведение «тихо», а добавляет явное.
Осторожно: декораторы — экспериментальная фича TypeScript. В каноничном ES-стандарте их роль занимают описания-декларации, поэтому в новых простых проектах иногда обходятся без них.
19. Типобезопасные ошибки: Result и Either
Исключения прерывают поток и «забывают» путь, по которому пришли. Альтернатива — значения-результаты: Result<T, E> (или «Either») явно несёт либо данные, либо ошибку, и типобезопасно обязывает оба случая обработать.
type Result<T, E> = { ok: true; value: T }
| { ok: false; error: E }
function findUser(id: string): Result<User, string> {
const user = db.get(id)
return user
? { ok: true, value: user }
: { ok: false, error: "Не найдено" }
}Разные виды неудач кодируются разными значениями ошибки внутри ветки { ok: false }, а на выходе обязательна исчерпывающая обработка: ветка для значения и ветка для ошибки всегда в наличии.
- компилятор не даст «забыть» обработать неудачу — это сильнее try/catch;
- тесты «ошибки» пишутся как данные, а не как отлов исключений;
- комбинация:
map,andThenсклеивают шаги без вложенных if.
20. tRPC: сквозная типизация без codegen
tRPC передаёт типы прямо из сервера на клиент: вместо API-слоя с генерацией схем — функции, вызываемые как локальные. Клиент «видит» сигнатуры серверных процедур на этапе компиляции.
// server/router.ts
export const appRouter = router({
getUser: procedure
.input(z.object({ id: z.string() }))
.query(({ input }) => findUser(input.id)),
});
// client
const { data } = trpc.getUser.useQuery({ id: "1" }); // TS-подсказки- один контракт: правка сервера сразу «ломает» клиент при несовпадении;
- валидация через zod в паре
input/output; - нет файла «типов API» и нет codegen — типы берутся из импорта.
Честная рамка: tRPC подходит, когда клиент и сервер в одном репозитории. Для публичного API или нескольких клиентов (iOS, сторонние) используют OpenAPI.
21. Типобезопасная работа с базой: Prisma и Drizzle
ORM с генерацией типов превращают базу в «типизированный источник»: схема таблиц становится типами, и запросы проверяются на этапе компиляции, а не в рантайме.
// schema.prisma (Prisma)
model User {
id Int @id @default(autoincrement())
email String @unique
tasks Task[]
}
// использования
const users = await prisma.user.findMany({
where: { email: { endsWith: "@acme.io" } },
}); // тип: User[]- миграции генерируются из схемы — схема и код не расходятся;
- Drizzle — «лёгкий» лайт-ORM с SQL-синтаксисом и типами из схемы;
- запросы автодополняются: колонки, relations, индексы — в редакторе.
Pitfall: генерация типов — шаг сборки, который стоит добавить в CI; иначе типы отстают от схемы. А для аналитических выборок ORM может не хватить — тогда SQL-фрагменты остаются типизированными.
Что дальше
Совмести типы с фреймворком: Vue 3 и React активно используют TS. Сборку настроишь через Vite, качество кода — курс ESLint, а тесты — Vitest. Основы языка без типов — в курсе JavaScript.