Три года назад TypeScript в вакансии «middle-фронтенд» писали в разделе «будет плюсом». Сейчас его пишут в разделе «требования», а без TS осталось разве что легаси, которое никто не хочет трогать. Node.js с версии 22.6 запускает .ts-файлы нативно (через --experimental-strip-types), Deno делает это без флагов, Bun — тоже. Дебаты «типы в JavaScript — хорошо или плохо» закончились. Индустрия проголосовала кодом.

Из-за этого поменялся и вопрос. Раньше стоял «зачем учить TS». Сейчас — «как войти правильно, чтобы не бросить на первом же дженерике». Разбираем на конкретных примерах.

Что такое TypeScript — на живых примерах

TypeScript — это надстройка над JavaScript, которая добавляет систему типов. Технически — отдельный язык, но синтаксис 95% совпадает с JS: любой валидный JS-файл — валидный TS-файл. Разница в том, что типы записываются явно, а компилятор проверяет их до запуска кода. В браузере или Node TypeScript сам не выполняется — компилятор tsc сначала стирает типы (получая обычный JS), потом уже этот JS исполняется.

// JavaScript — тип известен только в рантайме
function greet(name) {
  return `Привет, ${name.toUpperCase()}`;
}
greet(42);   // рантайм-ошибка: name.toUpperCase is not a function

// TypeScript — компилятор ловит несовместимость до запуска
function greetTs(name: string): string {
  return `Привет, ${name.toUpperCase()}`;
}
greetTs(42); // ← подчёркивает красным в IDE:
             // Argument of type 'number' is not assignable to parameter of type 'string'.

Ключевое отличие — когда именно ловится ошибка. В JS — когда пользователь уже открыл страницу и нажал на кнопку. В TS — когда программист ещё пишет код и IDE подчёркивает строку.

Из каких кирпичей строится система типов

Начальный набор, который покрывает 80% ежедневного кода.

Базовые типы. Примитивы string, number, boolean, null, undefined. Массивы через T[] или Array<T>. Объекты — через interface или type:

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  tags: string[];   // массив строк
  createdAt: Date;
}

// то же самое через type — почти равнозначно:
type UserT = {
  id: number;
  name: string;
  /* ... */
};

Объединения (union). Значение может быть одним из нескольких типов — классический случай, когда функция возвращает или данные, или ошибку.

type Status = 'pending' | 'success' | 'error';   // строковый литерал-union

function getStatus(): Status { return 'success'; }

// Значение может быть либо User, либо null (не пришло из API)
function findUser(id: number): User | null {
  return database.find(id) ?? null;
}

// Использование: TS заставит явно проверить null перед доступом
const user = findUser(1);
console.log(user.name);          // Error: user is possibly null
if (user) console.log(user.name); // OK

Дженерики (generics). Способ описать функцию или структуру, которая работает с любым типом, но сохраняет его для проверок. Массивы, промисы, коллекции — все построены на дженериках.

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstName = first(['Аня', 'Борис']);  // TS выводит: string | undefined
const firstNum = first([1, 2, 3]);           // TS выводит: number | undefined
firstName.toUpperCase();                      // OK — компилятор знает про string
firstNum.toUpperCase();                       // Error — нет метода toUpperCase у number

Что TS ловит, а что нет

Легко считать, что типы делают код неуязвимым. Не делают — они гарантируют одну конкретную вещь.

Ловит: опечатки в именах свойств (user.emial вместо user.email), несоответствие типов при вызовах функций, забытые ветки в switch над union-типом, доступ к возможно-null-значению без проверки, попытку использовать await на не-промис. Это большой класс ошибок, который в JS находят только через тесты или в проде.

Не ловит: рантайм-ошибки природы, которые в принципе не выразимы в типах. Fetch к сети упал — TS про это не узнает. JSON.parse получил на вход невалидный текст — тоже. Деление на ноль. Логические баги в бизнес-правилах. TS — про статическую корректность структур, а не про правильность решений.

Классический паттерн «проверенная полнота ветвлений» (exhaustiveness check) — штука, которую невозможно сделать в JS, а в TS она работает через never:

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'triangle'; base: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle':   return Math.PI * s.radius ** 2;
    case 'square':   return s.side ** 2;
    case 'triangle': return s.base * s.height / 2;
    default:
      // Если в Shape добавят новый вариант и забудут case здесь,
      // TS ругнётся: "Argument of type 'X' is not assignable to type 'never'"
      const _exhaust: never = s;
      throw new Error(`Unhandled shape: ${_exhaust}`);
  }
}

Добавили в тип Shape новую фигуру — компилятор автоматически ткнёт носом во все switch, где новый вариант не обработан. Это одна из главных вещей, ради которых люди уходят с JS и не возвращаются.

Что даёт TS, чего нет в чистом JS

Типы — это не только про проверки. Они меняют понимание кода редактором IDE.

Автокомплит по типам API. Написал user. — выпадает список именно тех полей, которые у объекта есть. Не список из всех идентификаторов проекта, как в JS-режиме, а точный набор. Никаких «а что там за поля были» и походов в другой файл.

Refactor-safe переименование. Правильно настроенная IDE (VSCode с TypeScript-плагином, IntelliJ) переименует user.nameuser.fullName во всём проекте ровно там, где это то же самое поле. Локальную переменную name в другом файле не тронет. В JS с этим справится только текстовый search-and-replace, который путает переменные.

Типы как живая документация. Файл types.ts с описанием форматов API — это спецификация, которую невозможно оставить устаревшей: если формат разошёлся с типом, компиляция падает. В JS такую спецификацию пишут в JSDoc или в отдельном README, и её никто не обновляет.

// types/api.ts — единственный источник правды про формат ответа API
export interface ArticleResponse {
  id: number;
  slug: string;
  title: string;
  publishedAt: string;   // ISO 8601
  tags: string[];
  author: {
    id: number;
    name: string;
  };
}

// в другом файле:
import { ArticleResponse } from './types/api';
async function fetchArticle(slug: string): Promise<ArticleResponse> {
  const r = await fetch(`/api/articles/${slug}`);
  return r.json();
  // возвращаемый тип уже проверен компилятором
}

Свежие фичи TS 5.x, которых нет в статьях прошлых лет

satisfies-оператор. Хочешь проверить, что значение соответствует типу, но сохранить его конкретный (литеральный) вид? В старом TS для этого использовали приведение as, которое ломает вывод типов — satisfies решает эту проблему.

type Colors = Record<string, string | [number, number, number]>;

// с "as" — компилятор забывает, что brand это литеральный кортеж
const palette1 = {
  brand: [14, 51, 111],
  accent: '#ffe066',
} as Colors;
palette1.brand[0]; // Error: Property '0' does not exist on type 'string | [number, number, number]'

// с "satisfies" — тип проверен, но конкретика сохранена
const palette2 = {
  brand: [14, 51, 111],
  accent: '#ffe066',
} satisfies Colors;
palette2.brand[0]; // OK, TS знает что brand — кортеж

const-параметры-типов. Позволяют вывести самый узкий возможный тип у аргумента-константы, не заставляя пользователя писать as const самому.

// без const
function pick<T extends string>(options: T[]): T { return options[0]; }
const a = pick(['red', 'green']);   // TS выводит: string

// с const
function pickC<const T extends string>(options: T[]): T { return options[0]; }
const b = pickC(['red', 'green']);  // TS выводит: 'red' | 'green' — точные литералы

Template literal types. Позволяют строить строковые типы из других типов — часто применяется в утилитах вроде маршрутизаторов, где адрес URL описывается через тип.

type Route = `/posts/${string}` | `/manuals/${string}/${string}`;
const r1: Route = '/posts/typescript-2026';                  // OK
const r2: Route = '/manuals/preobrazovanie-tipov/glava-1';   // OK
const r3: Route = '/random';                                   // Error

Когда TS избыточен

Не в каждой задаче типы окупаются — иногда чистый JS быстрее по времени и не хуже по надёжности.

  • Прототип на выходные — идея «посмотрю, что получится». Если прототип докажет себя — переведёшь позже. Если нет — сэкономил день на конфигах.
  • One-off скрипты — миграция данных, разовая обработка CSV, deploy-хелпер. Все они живут одну неделю, никого больше не касаются, ошибки локализованы.
  • Ad-hoc DevOps-код — глобальные обёртки для gh, docker, gcloud. Часто пишутся в bash или python-е, JS здесь — уже избыточен, TS — тем более.
  • Демо-код в статьях (вроде примеров в этой статье) — лишний шум типов затрудняет чтение. Иронично, но так.

Порог, за которым TS начинает окупаться — примерно 3-5 файлов и/или команда из двух и более человек. Дальше рост дивидендов резко ускоряется.

Как войти в TS в 2026 — путь миграции

Полный флип с JS на TS одномоментно — редко удачная идея. Рекомендуемый путь — постепенный, четырьмя шагами.

Шаг 1: JSDoc-типы в JS с проверкой. Добавляем в tsconfig.json флаг checkJs: true и пишем типы через JSDoc-комментарии. Файлы остаются .js, но TS-компилятор их проверяет.

// utils.js — обычный JS
/**
 * @param {number} n
 * @returns {string}
 */
function formatMoney(n) {
  return new Intl.NumberFormat('ru').format(n);
}
formatMoney("42");  // TS подсветит в IDE, хотя файл — .js

Шаг 2: смесь JS и TS. Флаг allowJs: true позволяет TS импортировать .js-файлы. Так можно постепенно переводить проект: новые файлы пишешь на TS, старые остаются JS, и всё это компилируется вместе.

Шаг 3: файл за файлом. Переименовываешь utils.js в utils.ts, добавляешь явные типы, фиксишь появившиеся ошибки. Дальше следующий файл. Так можно мигрировать big-codebase за несколько месяцев без блокировки разработки новых фич.

Шаг 4: включаешь strict: true. Финальный шаг — включить строгий режим, который ловит все any-неявности, null-небезопасности и т.д. Это уже полноценный TS-проект.

Что нужно из инструментов: tsc для компиляции (входит в пакет typescript), tsx или встроенный Node --experimental-strip-types для запуска .ts без предварительной компиляции. Раньше был ts-node — сейчас медленнее, но всё ещё работает. Vite / Next / Deno / Bun — в них TypeScript встроен по умолчанию.

Где пощупать. TypeScript Playground — онлайн-редактор от команды TS, где можно набрать сниппет, увидеть подсказки компилятора и результат транспиляции. Идеально для проверки идеи в один клик. Официальный туториал — TypeScript Handbook, читается за пару вечеров и покрывает 80% ежедневных задач.