ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Настройка tsconfig: strict, types и typeRoots без случайных глобальных типов

Автор: · Обновлено

Конфигурация TypeScript определяет, какие файлы проверяются, какие возможности окружения видны программе и какой JavaScript получается на выходе. Разберём небольшой проект для Node.js, в котором строгие проверки, глобальные типы и каталоги исходников заданы явно. Такой проект удобно воспроизвести и расширять без копирования сотни чужих параметров.

Цель и предварительные знания

После урока вы сможете объяснить каждую настройку небольшого tsconfig, отличить types от typeRoots и найти причину неожиданно доступной глобальной переменной. Нужны основы TypeScript, понимание undefined и опыт запуска команды в терминале. Пример рассчитан на Node.js 24 и TypeScript 6. Создайте отдельную учебную папку: менять настройки рабочего приложения ради эксперимента не требуется.

Минимальный проект с явным окружением

В корне сохраните package.json. Поле type определяет интерпретацию выходных .js файлов средой Node.js; оно не заменяет настройки компилятора. Для упражнения достаточно следующего содержимого.

{
  "private": true,
  "type": "module"
}

Установите компилятор и декларации Node.js. Полученный lockfile фиксирует точные версии для повторения эксперимента; каталог node_modules не является частью исходного кода.

pnpm add -D typescript@6 @types/node@24

Создайте tsconfig.json. Здесь нет сборщика: tsc проверяет программу и генерирует JavaScript, затем Node.js запускает результат.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "types": ["node"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src/**/*.ts"]
}

target задаёт уровень выходного синтаксиса, а lib — встроенные объявления стандартной библиотеки. Они не устанавливают полифиллы и не добавляют реальные методы в старый движок. NodeNext согласует работу компилятора с моделью модулей Node.js. Для приложения, которое собирается браузерным сборщиком, набор настроек будет другим: копировать этот файл целиком туда не следует.

Теперь создайте src/main.ts. Ожидаемые ошибки находятся в невыполняемой ветке и отмечены @ts-expect-error: компилятор проверяет наличие ошибки, но демонстрация не вызывает её в JavaScript.

const titles: string[] = ['Основы'];
const firstTitle = titles[0];
console.log(firstTitle?.toUpperCase() ?? 'Пусто');

type Draft = { note?: string };
const emptyDraft: Draft = {};
console.log('note' in emptyDraft);

if (false) {
  // @ts-expect-error Элемент по индексу может отсутствовать.
  const requiredTitle: string = titles[1];
  // @ts-expect-error Отсутствующее поле и явный undefined различаются.
  const ambiguousDraft: Draft = { note: undefined };
  console.log(requiredTitle, ambiguousDraft);
}
pnpm exec tsc -p tsconfig.json
node dist/main.js

Ожидаемый вывод — ОСНОВЫ, затем false. Первая строка использует безопасный доступ к потенциально отсутствующему элементу. Вторая подтверждает, что необязательное поле можно именно не передавать. Если протокол допускает явный undefined, это нужно выразить типом note?: string | undefined; для JSON такой вариант значения всё равно отсутствует.

Строгость состоит из разных решений

strict включает семейство проверок, в том числе контроль неявного any и отдельное отношение к null и undefined. Два дополнительных флага в примере заданы самостоятельно: не следует считать, что слово strict автоматически включает все полезные ограничения компилятора. Отключите каждый флаг отдельно и посмотрите, какая строка @ts-expect-error становится лишней.

При ошибке noEmitOnError запрещает новую генерацию выходных файлов. Уже лежащий в dist старый JavaScript при этом не исчезает. Поэтому запуск старого файла после неудачной сборки не доказывает исправность текущего исходника. В автоматизации команда запуска должна зависеть от успешного завершения компиляции.

types и typeRoots решают разные задачи

types перечисляет пакеты глобальных деклараций, которые нужно подключить: например, node для process или тестовый пакет для его глобальных функций. Это не список всех библиотек проекта. Типы явно импортируемого модуля по-прежнему разрешаются по обычным правилам импортов. В TypeScript 6 значение types по умолчанию изменилось на пустой список; явная запись помогает сделать окружение понятным независимо от истории обновлений.

typeRoots задаёт каталоги, внутри которых компилятор ищет пакеты деклараций. Если вы задаёте этот параметр, важно сохранить нужный путь к установленным @types. Для большинства приложений собственный typeRoots вообще не нужен: локальный .d.ts можно включить обычным include. Он полезен, когда действительно организована отдельная библиотека пакетов глобальных объявлений.

Предположим, учебный проект хранит такую библиотеку в types/app/index.d.ts. Тогда альтернативный фрагмент compilerOptions выглядит следующим образом; это дополнение к полной конфигурации, а не её самостоятельная замена.

{
  "types": ["node", "app"],
  "typeRoots": ["./types", "./node_modules/@types"]
}

Папка app здесь является именем пакета деклараций. Внутри можно описать согласованные глобальные типы, но объявление переменной через declare не создаст её при запуске. Не используйте typeRoots для коротких импортов @app/...: это задача разрешения модулей, рассматриваемая отдельно.

Типичные ошибки

Запуск tsc src/main.ts вместо tsc -p tsconfig.json меняет способ выбора конфигурации. Убедитесь, что проверяется именно проект. Ещё одна ошибка — воспринимать exclude как запрет на попадание файла в программу: исключённый из поиска include файл может прийти через импорт. Состав проекта проверяйте через --listFilesOnly, а итог унаследованных настроек — через --showConfig.

Не добавляйте DOM ради исчезновения сообщения об неизвестном браузерном объекте в серверном проекте. Сначала выясните, существует ли этот объект в реальной среде. И не включайте skipLibCheck как универсальное лечение конфликтов собственных типов: полезнее найти источник несовместимых деклараций и решить, какие версии должны сосуществовать.

Практика с разбором

Добавьте к Draft необязательное поле minutes, а затем функцию, печатающую его значение или фразу «не задано». Проверьте пустой объект, { minutes: 0 } и { minutes: 25 }. Отдельно объясните, должна ли запись { minutes: undefined } быть допустима по вашему контракту.

Решение использует draft.minutes ?? 'не задано': ноль сохраняется, потому что является содержательным значением. При minutes?: number и выбранном exactOptionalPropertyTypes явный undefined запрещён. Если вы намеренно меняете тип на minutes?: number | undefined, соответствующее ограничение снимается осознанно, а не отключением строгого режима для всего проекта.

Частые вопросы

Нужно ли новичку сразу настраивать typeRoots?

Обычно достаточно include для собственных файлов и types для нужного окружения. Добавляйте отдельные корни, когда можете назвать пакеты деклараций и объяснить, почему стандартный поиск их не покрывает. Иначе появляется дополнительная настройка без решаемой задачи.

Почему после успешной типизации программа всё равно падает?

Конфигурация помогает проверить совместимость кода с декларациями. Она не запускает программу, не создаёт отсутствующие API и не проверяет содержимое внешнего ответа. Ошибка загрузки модуля, неверные данные и нарушения предметных правил требуют собственных проверок в соответствующих местах.

Источники