TypeScript — JavaScript с типами

TypeScript — это надмножество JavaScript: весь валидный JS автоматически валиден и в TS. Главное отличие — статические типы, которые проверяются при компиляции. Ошибка «undefined is not a function» ловится ещё до запуска, а IDE получает полный автокомплит.

Уровень: средний 17 разделов ⏱ 3–4 часа

1. Что такое TypeScript

TypeScript разработан Microsoft. Код с расширением .ts компилируется в обычный JavaScript, который понимают все браузеры и Node.js. Времени выполнения у типов нет — они существуют только на этапе компиляции.

TypeScript позиционируют как «масштабируемый JavaScript»: на небольших скриптах типы могут показаться лишними, зато на проекте в десятки тысяч строк они окупаются многократно.

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;              // ошибка: число нельзя в переменную string
Компилятор TS по умолчанию ругается на несовместимые типы, но файл всё равно сгенерирует. Чтобы останавливаться на ошибке, включай noEmitOnError: 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");  // ок: строка имеет length

9. 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
// Пример модели ответа 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;
}

Декларации — публичный интерфейс библиотеки: обновляйте их вместе с функцией. Проверка этих файлов включается опцией skipLibCheck: false, а опечатки в них видны уже при первом использовании в проекте.

Не пишите any в декларациях «чтобы закомпилировалось»: такие типы обманывают и вас, и других разработчиков проекта.
Реальной типизации добивайтесь в ключевых местах: модели API, параметры функций, события. Видимые «обёртки» тип безопасности почти не добавляют, но замедляют всех.

13. Поэтапный перевод проекта на TS

Переводить существующий проект на TypeScript безопаснее постепенно: включить компилятор, починить ошибки, а потом ужестить параметры.

  1. Установите dev-зависимости и добавьте tsconfig.json с allowJs: true и checkJs: false.
  2. Переименуйте файлы в .ts .tsx по одному, начиная с новых модулей.
  3. Опишите модели данных (API-ответы, состояние) — они дают наибольшую пользу.
  4. Включите 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<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>;
Тип гарантирует форму, но не происхождение данных: проверяйте всё, что пришло по сети, особенно тело запроса и параметры URL.
Zod-схемы переиспользуйте и на клиенте: одна проверка формы и один выведенный тип — на обоих концах канала.

16. Производительность компиляции

Большой проект на TS начинает компилироваться медленно. Основные рычаги — точечность проверок, кэш и организация проекта, а не «выключение strict».

// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "skipLibCheck": true,
    "noEmit": true
  }
}

Куда проседают секунды: лишние include-папки, тяжёлые d.ts в node_modules, «любые» типы, заставляющие компилятор догадываться. Следите за временем одного tsc --noEmit в CI и не позволяйте ему расти незаметно.

Профилируйте компиляцию флагом --extendedDiagnostics: он покажет, где именно тратится время.
strict — не враг скорости: с incremental разовый запуск обычно быстрее, чем выключение проверок вручную. Точность важнее секунд на старте.

17. Общие типы фронта и бэка

Самая высокая цена типа — когда он один на клиент и сервер: единожды описанный контракт API не разъезжается при каждой правке.

// shared/types.ts — единый источник правды
export interface Order {
  id: string;
  totalCents: number;
  status: "pending" | "paid" | "cancelled";
}

export type CreateOrder = Omit<Order, "id" | "status">;
Не дублируйте типы вручную: копия контракта на клиенте и сервере рано или поздно «разъедется». Генерация и общий пакет — единственный честный путь.
Контракт валидируется на границе: zod-схема сервера + сгенерированный тип клиента дают обоюдную защиту.

18. Декораторы и расширения класса

Декораторы — функции-обёртки над классами, методами и свойствами. Их включают опцией experimentalDecorators в tsconfig; на практике они лежат в основе NestJS, TypeORM и аннотаций валидации.

function log(target: any, key: string) {
  console.log(`Метод вызван: ${key}`)
}

class User {
  @log
  save() { return "ok" }
}

Осторожно: декораторы — экспериментальная фича TypeScript. В каноничном ES-стандарте их роль занимают описания-декларации, поэтому в новых простых проектах иногда обходятся без них.

Не злоупотребляйте: три декоратора на методе затрудняют чтение. Метadata — для каркаса, но не для каждой мелочи.
Node без фреймворков декораторы малополезны — их сила именно в соглашениях каркасов (NestJS, TypeORM).

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 }, а на выходе обязательна исчерпывающая обработка: ветка для значения и ветка для ошибки всегда в наличии.

Начните с «собственного» Result без библиотеки — упражнение прояснит, когда он нужен, быстрее готового пакета.
Для консольных/простых программ исключения удобнее; Result окупается в серверном коде и сложных бизнес-цепочках.

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-подсказки

Честная рамка: tRPC подходит, когда клиент и сервер в одном репозитории. Для публичного API или нескольких клиентов (iOS, сторонние) используют OpenAPI.

Начните с query + одна mutation: почувствуете удобство — расширяйте роутер так же, как код модуля.
Авторизация — через middleware процедур: tRPC поддерживает контекст и защиту роутов на сервере.
Не «переезжайте» REST-проект в tRPC ради моды: публичный API или микросервисы остаются на классических контрактах.

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[]

Pitfall: генерация типов — шаг сборки, который стоит добавить в CI; иначе типы отстают от схемы. А для аналитических выборок ORM может не хватить — тогда SQL-фрагменты остаются типизированными.

Выбирайте между Prisma (комфорт, экосистема) и Drizzle (скорость, SQL-близость) по задачам проекта, а не по популярности.
Не доверяйте схеме «на слово»: типы генерируются из миграций — держите их в репозитории и проверяйте в CI.

Что дальше

Совмести типы с фреймворком: Vue 3 и React активно используют TS. Сборку настроишь через Vite, качество кода — курс ESLint, а тесты — Vitest. Основы языка без типов — в курсе JavaScript.

← Вернуться к курсам