open atlas
↑ К треку
Система типов TypeScript вглубь TS · 07 · 05

Проверка типов против транспиляции: почему твой бандлер не ловит ошибки типов

esbuild/swc/babel транспилируют пофайлово и НЕ проверяют типы — ошибка типа не остановит бандл. Эта модель требует `isolatedModules` (запрет const enum между файлами, нужен `export type`). Паттерн: бандлер собирает, а `tsc --noEmit` — отдельный гейт типов в CI.

TS Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Коллега выкатил код, где const total: number = someUserId; — присваивание строки в number. Vite собрал без единого писка, приложение задеплоилось, и баг вылез только в production, когда арифметика вернула NaN. «Но это же TypeScript!» — да, но Vite использует esbuild, который транспилирует TypeScript и никогда не проверяет типы. Аннотация типа была удалена, а не проверена. Ничто в конвейере сборки не запускало проверщик типов.

Две совершенно разные задачи: проверка против транспиляции

tsc делает две вещи: проверяет типы (анализирует всю программу на ошибки типов) и генерирует JavaScript. Бандлеры разделяют их и оставляют только вторую:

инструментпроверяет типы?emitскоростьвидит другие файлы?
tscда (вся программа)JS + .d.tsмедленнода — межфайловый анализ
esbuildнетJSочень быстронет — по одному файлу
swcнетJSочень быстронет — по одному файлу
babel (preset-typescript)нетJSбыстронет — по одному файлу
tsc --noEmitданичегомедленнода — CI-гейт типов

Причина, по которой бандлеры в 10–100× быстрее tsc, именно в том, что они не проверяют типы. Анализ типов требует построения графа всей программы; транспиляция просто удаляет синтаксис типов токен за токеном в одном файле. Ошибка типа им невидима — это просто синтаксис, который они стирают.

isolatedModules: делаем однофайловую транспиляцию безопасной

Если твоя команда использует esbuild или swc для emit, есть целый класс валидного TypeScript, который молча сгенерирует неправильный JavaScript — не ошибку, а просто сломанный бандл. isolatedModules — флаг, который ловит такие конструкции в редакторе, а не в рантайме.

Поскольку esbuild/swc/babel компилируют по одному файлу за раз, не зная о других файлах, некоторые конструкции TypeScript небезопасно так транспилировать — им нужна информация о всей программе. isolatedModules: true заставляет tsc помечать именно такие конструкции, чтобы твой код был портируем на однофайловый транспилятор:

// с isolatedModules эти конструкции дают ошибку в tsc:

// 1. type-only реэкспорт, ВЫГЛЯДЯЩИЙ как реэкспорт значения:
export { SomeType } from "./types";
//       ^^ ~TS1205: однофайловый компилятор не может понять, SomeType — это
//          тип (стереть экспорт) или значение (оставить). Фикс:
export type { SomeType } from "./types"; // теперь однозначно — стереть

// 2. const enum между файлами — нужны значения enum, вшитые из другого файла:
import { Color } from "./enums";
const c = Color.Red; // const enum нельзя вшить без межфайловой информации

isolatedModules не меняет emit; он ограничивает то, что можно писать, чтобы пофайловая транспиляция давала корректный вывод. Две главные вещи, которые он фактически запрещает: const enum, потребляемый между файлами (однофайловый компилятор не может вшить значения, которых не видит) и неоднозначные type-only реэкспорты (вынуждая export type / import type).

Почему это работает

Почему однофайловый транспилятор не справляется с export { SomeType } from "./types"? Потому что для генерации корректного JS он должен решить, SomeType — это тип (стереть реэкспорт целиком — типы не существуют в рантайме) или значение (оставить реэкспорт). Это решение требует открыть ./types и проверить, что такое SomeType — межфайловое знание, которое транспилятор ради скорости собирать отказывается. export type { SomeType } говорит ему однозначно «это тип, сотри», поэтому isolatedModulesverbatimModuleSyntax) подталкивают к явным модификаторам type.

Паттерн: бандлер собирает, tsc --noEmit гейтит

Production-настройка развязывает две задачи. Бандлер быстро производит артефакт; tsc --noEmit (часто tsc -b --noEmit для монорепо) запускается параллельно или в CI чисто чтобы проверить типы — он ничего не генерирует, лишь валит сборку, если тип неверен:

// скрипты package.json
{
  "scripts": {
    "build": "vite build",            // esbuild транспилирует — БЕЗ проверки типов
    "typecheck": "tsc --noEmit",      // настоящий гейт типов — ничего не генерирует
    "ci": "npm run typecheck && npm run build"
  }
}

verbatimModuleSyntax (TS 5.0) усиливает это: он делает emit import/export буквальным, так что обычный import всегда сохраняется, а стираются только import type/export type. Эта предсказуемость и нужна транспилятору — никакого угадывания, был ли импорт «использован как значение». Он заменяет старый importsNotUsedAsValues и закрывает те же однофайловые заботы, которые помечает isolatedModules. (Заметь: сам tsc всё ещё делает declaration emit — генерацию .d.ts — чего транспилятор не делает никогда, поэтому сборки библиотек оставляют tsc для типов, даже когда JS производит esbuild.)

Закончи аналогию

Транспилятор вроде esbuild относится к проверке типов так же, как быстрый копировальный аппарат относится к ___ — он быстро воспроизводит страницы, но никогда не читает их на ошибки.

Викторина

Твоё Vite-приложение собирается и деплоится, хотя в файле явная ошибка типа (присваивание строки в number). Почему сборка не упала и как это поймать?

Вспомните перед уходом
  1. 01
    Почему ошибка типа проходит сквозь сборку esbuild/swc/babel и каков фикс?
  2. 02
    Что такое isolatedModules, почему он нужен однофайловым транспиляторам и что он запрещает?
  3. 03
    Что за паттерн бандлер-плюс-tsc-noEmit и где тут verbatimModuleSyntax / declaration emit?
Итог

Теперь ты знаешь разделение труда, определяющее современную сборку TypeScript: транспиляторы генерируют JS пофайлово и никогда не проверяют типы, isolatedModules держит код безопасным для этой модели (нет межфайловых const enum, явный export type), а tsc --noEmit — гейт, реально обеспечивающий безопасность типов, пока бандлер отвечает за скорость. Это закрывает юнит config/modules/build. Дальше, в TypeScript на практике, мы применяем всё это — частые ловушки, где эти тонкости конфига и emit бьют команды в production-коде. Теперь, когда видишь ошибку типа, проскользнувшую в production через Vite-сборку, знаешь, чего не хватало: tsc --noEmit никогда не был в CI-пайплайне.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.