lenec ru

← все посты

Strict TypeScript на проекте: какие флаги дают ценность, а какие шумят

13K

Я работаю с TypeScript шесть лет, и за это время три раза включала strict: true на чужих проектах. Все три раза первая реакция команды была одна: «у нас же десять тысяч ошибок, мы что, всё это будем чинить?». Все три раза через месяц никто не хотел возвращаться обратно.

Но это не значит, что надо тупо включать всё подряд. Часть строгих флагов реально ловит баги. Часть — добавляет шума, в котором тонут настоящие ошибки. Расскажу, какие я ставлю по умолчанию на каждый новый проект, какие включаю с осторожностью, а какие принципиально не использую.

Что вообще делает strict

Когда ты ставишь "strict": true в tsconfig.json, это включает пакет из примерно десяти флагов: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict. Хорошая новость — большинство из них вы и так хотите. Плохая — флаги вне strict тоже бывают полезными, но про них часто забывают.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false
  }
}

Пройдусь по каждому интересному флагу с реальными примерами.

Флаги, которые я всегда включаю

strictNullChecks

Самый ценный флаг во всём TypeScript. Без него null и undefined можно присвоить куда угодно, и компилятор молчит. С ним каждый кейс «а вдруг тут пусто» вылезает на этапе сборки.

function getUser(id: string): User | null {
  return users.find((u) => u.id === id) ?? null;
}

const user = getUser("42");
console.log(user.name); // ❌ Object is possibly null

Это тот самый флаг, ради которого вообще стоит брать TypeScript. Если у вас он отключён, всё остальное — фасад.

noImplicitAny

Запрещает неявный any. Если у функции не указан тип параметра, компилятор не молчит, а кричит. Помогает не оставлять дыры в типизации по случайности.

noUncheckedIndexedAccess

Не входит в strict, но я его ставлю всегда. Без него обращение по индексу или ключу возвращает T, как будто в массиве всегда что-то есть. С ним возвращает T | undefined.

const items = ["a", "b", "c"];
const first = items[0]; // string | undefined
const record: Record<string, number> = {};
const value = record.foo; // number | undefined

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

noFallthroughCasesInSwitch

Запрещает провалы между case в switch. Я не помню ни одного случая, когда провал был бы намеренным. Зато случайные забытые break и return ловила десятки раз. Включай не задумываясь.

noImplicitOverride

Если ты переопределяешь метод родителя в наследнике, обязан написать override. Маленький флаг, но в коде с классами спасает от ситуации «родитель переименовал метод, а у тебя в наследнике осталась старая версия и компилятор молчит». На фронте я почти не работаю с классами, но в командах, где есть OOP-стиль, это спасало проекты.

Флаги из strict, по которым стоит подумать

strictPropertyInitialization

Заставляет инициализировать все non-optional поля класса в конструкторе. Полезно для классов с состоянием, бесполезно для DI-сервисов, где поля инициализируются фреймворком.

class UserService {
  // ❌ Property has no initializer and is not definitely assigned
  private repo: UserRepository;
}

В таких случаях либо ставлю ! (definite assignment assertion), либо переписываю на функциональный стиль. На React-проекте этот флаг почти никогда не срабатывает, потому что классов нет.

useUnknownInCatchVariables

В catch (e) переменная теперь unknown, а не any. Это правильно — ты реально не знаешь, что там бросили. Но в первый раз больно: весь код, где было e.message, ломается.

try {
  doSomething();
} catch (e) {
  if (e instanceof Error) {
    console.error(e.message);
  } else {
    console.error("unknown error", e);
  }
}

Включаю всегда. Это правильное поведение, просто на старом коде требует вечера на чистку.

Флаги, к которым у меня сложные отношения

exactOptionalPropertyTypes

Самый спорный флаг в моей карьере. По умолчанию field?: string означает string | undefined. С этим флагом — только string или вообще не указывать поле. Звучит логично. На практике ломает половину сторонних библиотек и заставляет писать так:

type Props = {
  title?: string;
};

// ❌ при exactOptionalPropertyTypes
const props: Props = { title: undefined };

// ✅ нужно так
const props: Props = {};
// или так
const props: Props = condition ? { title: "hi" } : {};

На внутренних типах — отлично. Но как только ты передаёшь данные в react-hook-form, в zod, в API-клиент, который сериализует undefined в JSON — начинаются танцы. На последнем проекте я выключила его через две недели и не пожалела. На библиотечном коде — оставила бы.

noUnusedLocals и noUnusedParameters

Запрещают неиспользованные переменные и параметры. Кажется, что это must-have, но я давно их выключила и оставила только в ESLint.

Причина простая: эти флаги мешают рефакторингу. Закомментировал кусок кода, чтобы быстро проверить — TypeScript уже красный, проект не собирается. В ESLint можно настроить ignore для подчёркивания (_unused), и поведение становится живее.

strictFunctionTypes и strictBindCallApply

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

Флаги, которые я включаю на новых проектах, но не вношу в legacy

Разница важная. На зелёном проекте можно сразу с первого дня включить максимум — стоимость 5 минут. На проекте с парой сотен файлов та же операция может занять неделю и привести к выгоранию команды.

Стратегия для legacy: включать строгие флаги по одному, отдельной задачей, через // @ts-expect-error на оставшихся местах, и с обязательным разбором каждой ошибки в ревью. Не пытайтесь героически за один спринт перевести проект на full strict. Я однажды попробовала — закрыла 800 ошибок за выходные и пришла на следующий понедельник с глазом, который дёргался.

Что включить ещё, помимо tsconfig

Несколько ESLint-правил, которые в паре с TypeScript ловят то, что чисто типы не ловят:

  • @typescript-eslint/no-floating-promises — забытый await. Самый частый источник тихих багов.
  • @typescript-eslint/no-misused-promises — async-функция как обработчик там, где ждут синхронный.
  • @typescript-eslint/switch-exhaustiveness-check — проверка, что switch покрывает все варианты дискриминированного объединения.
  • @typescript-eslint/consistent-type-imports — отдельный синтаксис import type, чтобы тайпы не утекали в рантайм-бандл.

Эти правила не связаны со strict напрямую, но дают ту же ценность: ловят то, что глазами не видно.

Минимальная и максимальная конфигурация

Чтобы было от чего оттолкнуться. Минимум, на котором я готова работать:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Максимум, который я ставлю на новый проект, где могу контролировать всё:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Заметь: ни exactOptionalPropertyTypes, ни noUnusedLocals. Первый — потому что ломает интеграции, второй — потому что есть в ESLint и там удобнее.

Что запомнить

Strict-флаги — не «больше всегда лучше». Это инструменты под конкретные задачи. Базовый набор (strict + noUncheckedIndexedAccess + noFallthroughCasesInSwitch) ставь сразу, без обсуждения. Дальше — смотри по проекту: где-то exactOptionalPropertyTypes сэкономит часы дебага, где-то наоборот съест неделю на правки в типах сторонних либ.

Главное — не путать строгость с шумом. Если флаг даёт пять ошибок «бага бы не было, но компилятор истерит» на одну реальную — выключай. Цель TypeScript не в том, чтобы быть строгим, а в том, чтобы ты ночью спал спокойно.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

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