$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогНачинаем с TypeScript правильно: строгие настройки и защита от самых частых багов

Начинаем с TypeScript правильно: строгие настройки и защита от самых частых багов

$ sudo teach IT
·19 июля 2026 г.·10 мин·57
Начинаем с TypeScript правильно: строгие настройки и защита от самых частых багов

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

Содержание
Почему «просто TypeScript» не защищает от баговСтрогие настройки: включаем правильный набор и понимаем последствияВключаем strictПолный набор: strict + важные нюансыДоменная модель через типы: меньше «магии» в объектахПример: Заказ и статусыЛокальная защита от багов: обработка входных данныхТипы функций: как не допускать «непонятных» входов и выходовПроблема: any в краях системыПолезный приём: универсальные сигнатуры без «лишней свободы»Защита от частых багов: список проблем и точечные решенияБаг №1: undefined на ровном местеБаг №2: неучтённые ветви switchБаг №3: смешивание «числа» и «строки» (а иногда и единиц измерения)Баг №4: мутация данных и неожиданные побочные эффектыПрактический пример: улучшаем код по шагам (от слабого к строгому)Вариант 1: «всё как есть»Вариант 2: дискриминируем статусыВариант 3: отделяем валидацию от бизнес-логикиТипы и “стоимость ошибки”: где именно TS даёт максимум эффектаКак не утонуть в типах: практичные правилаИтог: строгие настройки и типы как архитектурный инструмент

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

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


Почему «просто TypeScript» не защищает от багов

У многих проектов TS включён формально: noImplicitAny выключен, strict не включён, типы местами размазаны в any, а проверки сводятся к минимальному пересечению типов. В такой конфигурации TypeScript перестаёт быть «страховкой» и превращается в косметику.

Типичная картина:

  • Функции получают any или unknown без нормализации входных данных.
  • Ошибки в структуре объекта ловятся только во время выполнения.
  • Доменная модель (например, «Заказ», «Платёж», «Пользователь») живёт в виде свободных plain-object без инвариантов.
  • Логика ветвится по полям, которые могут быть undefined — но это не отражено в типах.

Строгие настройки + нормальные типы решают эту проблему: вы «переносите» часть проверок из рантайма в компиляцию.


Строгие настройки: включаем правильный набор и понимаем последствия

Включаем strict

Минимальная цель — включить строгий режим:

code
{
  "compilerOptions": {
    "strict": true
  }
}

Но strict — это агрегатор. Он включает несколько флагов, которые по-разному влияют на качество. Разберём ключевые.

Полный набор: strict + важные нюансы

Рекомендуемый базовый tsconfig.json для старта (в большинстве случаев — почти без компромиссов):

code
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",

    "strict": true,

    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    "forceConsistentCasingInFileNames": true,

    "skipLibCheck": true,

    "noEmit": true
  }
}

noUncheckedIndexedAccess

Это один из самых практичных флагов. Без него доступ к массиву и объектам по индексу/ключу может считаться «гарантированно существующим», хотя на самом деле это не так.

Пример:

code
const arr = ["a", "b"];
const first = arr[0]; // string
const third = arr[2]; // string (в слабых настройках, что неверно)

С включённым noUncheckedIndexedAccess будет:

  • arr[0] → string | undefined
  • arr[2] → string | undefined

Это заставляет вас явно обработать отсутствие значения.

exactOptionalPropertyTypes

Без него опциональные свойства (foo?: string) часто ведут себя «мягко» и могут скрыть ошибки в доменной модели. С exactOptionalPropertyTypes foo?: string становится точнее: свойство может быть отсутствующим, и это отражается в типах.

Например, типы для объекта оплаты:

code
type Payment = {
  id: string;
  status: "pending" | "paid";
  receiptUrl?: string; // может отсутствовать
};

Если в коде вы используете receiptUrl как строку без проверки, компилятор подскажет, что это небезопасно.

skipLibCheck

Включение skipLibCheck обычно не снижает качество ваших проверок, но ускоряет сборку, потому что не компилируются типы из node_modules. На старте это удобно: вы концентрируетесь на своём коде.


Доменная модель через типы: меньше «магии» в объектах

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

Ключевой принцип: если у доменного объекта есть инварианты — выражайте их в типах.

Пример: Заказ и статусы

Пусть есть сущность заказа. В реальном мире статусы и связанные поля зависят друг от друга.

Плохая модель (слишком свободная)

code
type Order = {
  id: string;
  status: "created" | "paid" | "shipped";
  shippedAt?: string;
  paidAt?: string;
};

Проблема не в том, что это «не компилируется». Проблема в том, что модель позволяет нелепые состояния:

  • status: "paid" при этом paidAt отсутствует.
  • status: "shipped" при этом shippedAt не задан.

TypeScript это не остановит.

Хорошая модель: дискриминируемые объединения (discriminated unions)

Правильнее выразить зависимость полей от статуса:

code
type OrderCreated = {
  id: string;
  status: "created";
};

type OrderPaid = {
  id: string;
  status: "paid";
  paidAt: string; // обязательно
};

type OrderShipped = {
  id: string;
  status: "shipped";
  paidAt: string;   // так как платёж был до отправки
  shippedAt: string;
};

type Order = OrderCreated | OrderPaid | OrderShipped;

Теперь компилятор сможет помогать вам в ветвлениях.

Например, обработчик:

code
function canCancel(order: Order): boolean {
  switch (order.status) {
    case "created":
      return true;
    case "paid":
    case "shipped":
      return false;
  }
}

Если вы добавите новый статус — компилятор заставит проверить switch. А попытка использовать order.shippedAt вне статуса будет ошибкой.

Локальная защита от багов: обработка входных данных

Многие ошибки в рантайме происходят на границе системы: при чтении из API/БД/файла вы получаете данные, которые не обязаны соответствовать вашему типу.

TypeScript не проверяет данные во время выполнения — он лишь проверяет типы в вашем коде. Поэтому, когда вы получаете unknown/any извне, нужно делать валидацию.

Пример: функция парсинга заказа.

code
function assertOrder(input: unknown): Order {
  if (typeof input !== "object" || input === null) {
    throw new Error("Order must be an object");
  }

  const obj = input as Record<string, unknown>;
  const id = obj.id;
  const status = obj.status;

  if (typeof id !== "string") throw new Error("Order.id must be string");
  if (status !== "created" && status !== "paid" && status !== "shipped") {
    throw new Error("Invalid order.status");
  }

  if (status === "created") {
    return { id, status };
  }

  if (status === "paid") {
    const paidAt = obj.paidAt;
    if (typeof paidAt !== "string") throw new Error("paidAt must be string");
    return { id, status, paidAt };
  }

  // status === "shipped"
  const paidAt = obj.paidAt;
  const shippedAt = obj.shippedAt;

  if (typeof paidAt !== "string") throw new Error("paidAt must be string");
  if (typeof shippedAt !== "string") throw new Error("shippedAt must be string");

  return { id, status, paidAt, shippedAt };
}

Да, это чуть длиннее. Но это как раз тот случай, когда вы платите временем разработчика один раз — чтобы больше не разбираться с «случайными» падениями в проде.


Типы функций: как не допускать «непонятных» входов и выходов

Даже с нормальной доменной моделью типы функций часто превращаются в источник ошибок.

Проблема: any в краях системы

Обычно any появляется там, где вы не контролируете источник: fetch, request.body, сериализация и т. п.

Опасный паттерн:

code
function handle(req: any) {
  // ...
}

Лучше:

  1. Считать вход как unknown.
  2. Применить проверку.
  3. Дальше работать уже с корректными типами.

Пример:

code
function handleBody(body: unknown) {
  const data = assertOrder(body); // либо другая assert-логика
  // теперь data: Order
}

Полезный приём: универсальные сигнатуры без «лишней свободы»

Если ваша функция принимает коллбек, часто нужно аккуратно ограничить типы параметров. Например, функция для обработки событий:

code
type Event =
  | { type: "created"; orderId: string }
  | { type: "paid"; orderId: string; paidAt: string }
  | { type: "shipped"; orderId: string; shippedAt: string };

function dispatch<T extends Event>(event: T): T {
  // например, логируем и возвращаем
  return event;
}

Такая сигнатура сохраняет конкретный тип события (а не скатывается в общий Event).


Защита от частых багов: список проблем и точечные решения

Баг №1: undefined на ровном месте

С strictNullChecks и noUncheckedIndexedAccess компилятор будет ругаться на опасные места. Но есть и человеческий фактор: разработчик может «обойти» систему типов.

Плохой паттерн:

code
const name = user.name!;

! — это обещание компилятору, что значение не null/undefined. Если вы ошиблись — получаете рантайм-краш.

Лучше — явная проверка:

code
function formatUser(user: { name?: string }) {
  if (!user.name) return "Anonymous";
  return user.name;
}

Баг №2: неучтённые ветви switch

В хорошей модели (discriminated unions) switch — мощный инструмент. Но нужно писать его так, чтобы компилятор мог помочь.

Приём: never в финальной ветви.

code
function orderMessage(order: Order): string {
  switch (order.status) {
    case "created":
      return "Order is created";
    case "paid":
      return `Paid at ${order.paidAt}`;
    case "shipped":
      return `Shipped at ${order.shippedAt}`;
    default: {
      const _exhaustive: never = order;
      return _exhaustive;
    }
  }
}

Если появится новый статус и вы забудете обработку — TypeScript перестанет компилировать код.

Баг №3: смешивание «числа» и «строки» (а иногда и единиц измерения)

В JS легко перепутать amount (число) и currency (строка), или ms и s.

TypeScript частично лечит это приёмами «брендирования» (nominal typing через & { __brand: ... }).

code
type Money = number & { readonly __brand: "Money" };
type Milliseconds = number & { readonly __brand: "Ms" };

function ms(value: number): Milliseconds {
  return value as Milliseconds;
}

function money(value: number): Money {
  return value as Money;
}

Да, это не «волшебная таблетка», но это хороший стоп-кран для простых ошибок.

Баг №4: мутация данных и неожиданные побочные эффекты

Типы могут помочь и здесь: делайте значения неизменяемыми, где это уместно.

code
type OrderView = Readonly<{
  id: string;
  status: Order["status"];
}>;

Или на уровне массива/структур — использовать ReadonlyArray<T>.


Практический пример: улучшаем код по шагам (от слабого к строгому)

Возьмём типичный фрагмент: получение списка заказов и формирование отчёта.

Вариант 1: «всё как есть»

code
type OrderRaw = {
  id: string;
  status: string;
  paidAt?: string;
  shippedAt?: string;
};

function buildReport(orders: OrderRaw[]) {
  return orders.map((o) => ({
    id: o.id,
    status: o.status,
    shippedAt: o.shippedAt?.toUpperCase() ?? "N/A"
  }));
}

Что здесь не так:

  • status — строка, и мы не знаем допустимые значения.
  • Мы используем shippedAt как будто это строка, но она может быть отсутствующей — мы подставляем N/A, но это может скрывать ошибки источника данных.
  • Нет инвариантов между статусом и датами.

Вариант 2: дискриминируем статусы

code
type OrderCreated = { id: string; status: "created" };
type OrderPaid = { id: string; status: "paid"; paidAt: string };
type OrderShipped = {
  id: string;
  status: "shipped";
  paidAt: string;
  shippedAt: string;
};

type Order = OrderCreated | OrderPaid | OrderShipped;

function buildReport(orders: Order[]) {
  return orders.map((o) => {
    switch (o.status) {
      case "created":
        return { id: o.id, status: o.status, shippedAt: "N/A" };
      case "paid":
        return { id: o.id, status: o.status, shippedAt: "N/A" };
      case "shipped":
        return { id: o.id, status: o.status, shippedAt: o.shippedAt.toUpperCase() };
    }
  });
}

Теперь:

  • Невозможно создать Order со статусом "paid" без paidAt.
  • В switch обработка полностью покрывает статусы.
  • Ошибки в данных обнаружатся на этапе assertOrder (когда вы делаете преобразование входа к доменной модели), а не в середине логики.

Вариант 3: отделяем валидацию от бизнес-логики

code
function buildReportFromUnknown(input: unknown) {
  const orders = assertOrders(input); // здесь вы делаете проверки
  return buildReport(orders);
}

Компоновка:

  • Валидация — на границе.
  • Бизнес-логика — на типах доменной модели.
  • Это заметно снижает сложность отладки.

Типы и “стоимость ошибки”: где именно TS даёт максимум эффекта

Строгие настройки имеют «цены» в виде необходимости писать больше кода (особенно при работе с внешними данными). Но они окупаются, когда вы:

  • регулярно интегрируетесь с внешними API,
  • активно используете switch по типам/статусам,
  • храните важные инварианты в доменных сущностях,
  • много рефакторите и меняете структуру данных.

Если же проект маленький и вы контролируете весь код от начала до конца, TS всё равно полезен, но эффект может быть менее заметным.


Как не утонуть в типах: практичные правила

  1. Не используйте any как универсальную “отмазку”. Если сомневаетесь — используйте unknown и потом проверяйте.
  2. Разделяйте границы и внутренности: на границах — валидация; внутри — строгие типы доменной модели.
  3. Сделайте switch исчерпывающим с never, когда вы работаете с discriminated unions.
  4. Не размывайте инварианты: если поле обязательно при статусе — оно должно быть обязательно в типе.
  5. Включайте строгие флаги заранее. Добавлять их в середине проекта часто болезненно, потому что вы обнаруживаете сотни мест, которые раньше “проходили”.

Итог: строгие настройки и типы как архитектурный инструмент

Правильный старт с TypeScript — это не попытка «добавить типы любой ценой». Это дисциплина:

  • включить строгие настройки (strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes);
  • выразить инварианты предметной области через дискриминируемые объединения;
  • отделить валидацию входных данных от бизнес-логики;
  • писать ветвления так, чтобы компилятор защищал вас от неучтённых случаев.

Если вы хотите закрепить это системно, полезным следующим шагом может стать курс «TypeScript - для начинающих!» — как способ собрать базу и разобраться в типовых паттернах уже с примерами и разбором ошибок.

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

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

TypeScript - для начинающих!

Курс по теме

TypeScript - для начинающих!

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

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API
модели

Модели данных и типизация в TypeScript: как проектировать контракт между фронтом и API

Разберём, как описывать доменные сущности, DTO и error-model на уровне типов, чтобы ловить несовпадения схем до продакшена. Покажем практические шаблоны для типобезопасного маппинга и версионирования контрактов.

15 сентября 2026 г.
1050
TypeScript vs JavaScript: когда типы спасают проект, а когда мешают
typescript

TypeScript vs JavaScript: когда типы спасают проект, а когда мешают

Сравниваем подходы на реальных примерах кода, разбираем, при каких размерах и архитектурах проекта TypeScript окупается, а когда достаточно JSDoc.

16 июля 2026 г.
740
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Почему «просто TypeScript» не защищает от баговСтрогие настройки: включаем правильный набор и понимаем последствияВключаем strictПолный набор: strict + важные нюансыДоменная модель через типы: меньше «магии» в объектахПример: Заказ и статусыЛокальная защита от багов: обработка входных данныхТипы функций: как не допускать «непонятных» входов и выходовПроблема: any в краях системыПолезный приём: универсальные сигнатуры без «лишней свободы»Защита от частых багов: список проблем и точечные решенияБаг №1: undefined на ровном местеБаг №2: неучтённые ветви switchБаг №3: смешивание «числа» и «строки» (а иногда и единиц измерения)Баг №4: мутация данных и неожиданные побочные эффектыПрактический пример: улучшаем код по шагам (от слабого к строгому)Вариант 1: «всё как есть»Вариант 2: дискриминируем статусыВариант 3: отделяем валидацию от бизнес-логикиТипы и “стоимость ошибки”: где именно TS даёт максимум эффектаКак не утонуть в типах: практичные правилаИтог: строгие настройки и типы как архитектурный инструмент