Установка и три способа запустить .ts
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Компилятор TypeScript — обычный npm-пакет, а не программа, которую ставят в систему. Установка занимает одну команду; дальше начинается интересное. Запустить .ts-файл можно тремя разными способами, и только один из них вообще смотрит на типы — остальные молча их стирают.
TypeScript ставится в проект как любая другая зависимость разработки: npm install -D typescript. Флаг -D здесь по делу — в готовую сборку компилятор не уезжает, он делает свою работу до запуска и на этом заканчивается. После установки в node_modules/.bin появляется команда tsc, TypeScript Compiler.
Звать её нужно через npx tsc или через скрипт в package.json: и то и другое берёт бинарник из node_modules текущего проекта. Глобальная установка выглядит удобнее ровно до того дня, когда у вас на машине шестая версия, а в lock-файле проекта пятая, — и вы видите ошибки, которых не видит сборка на сервере, или наоборот. Версия компилятора — такая же часть проекта, как версия любого пакета; тот же довод, что и про lock-файл в главе про npm.
| Команда | Что делает |
|---|---|
| npm install -D typescript | ставит компилятор в проект |
| npx tsc --version | печатает версию — проверка, что всё встало |
| npx tsc --init | создаёт заготовку tsconfig.json |
| npx tsc | собирает проект по конфигу |
| npx tsc --noEmit | только проверяет типы, ничего не пишет |
| npx tsc --watch | перепроверяет при каждом сохранении |
У pnpm и yarn те же команды с другим префиксом: pnpm add -D typescript и pnpm exec tsc, yarn add -D typescript и yarn tsc.
Первый файл на TypeScript выглядит как обычный JavaScript, которому дописали пару слов после двоеточия. Всё, что вы писали в частях I и II, остаётся валидным кодом: TypeScript — надстройка, а не другой язык.
Компилятор промолчал — значит, придраться было не к чему. И ещё одно: диагностики, которые вы будете видеть под примерами, не нарисованы в тексте — их выдаёт настоящий компилятор, и по-русски он говорит потому, что везёт переводы с собой. Только версия у него не та, что вы сейчас поставили. Сегодня npm ставит TypeScript 7 — нативный компилятор. Песочница учебника работает на 6.0.3: ей нужен JS API, чтобы звать компилятор прямо в браузере, а у седьмой версии такого API в прежнем виде нет. Ошибки в самих программах у двух версий те же — с теми же кодами и смыслом, так что ошибку из песочницы вы узнаете в своём терминале по коду: там tsc по умолчанию говорит по-английски.
Компилятору надо знать, какие файлы брать, во что их превращать и насколько придираться. Всё это лежит в tsconfig.json в корне проекта, а заготовку делает npx tsc --init.
Когда-то эта команда вываливала полотно из полутора сотен закомментированных строк, среди которых новичку было нечего выбирать. Сегодня файл короткий, и почти всё в нём уже включено — авторы языка перестали делать вид, что строгость опциональна.
| Строка из свежего конфига | Что она значит |
|---|---|
| "strict": true | включает сразу восемь проверок; главная — запрет неявного any |
| "target": "esnext" | в каком JavaScript писать результат |
| "module": "nodenext" | как понимать import и export |
| "noUncheckedIndexedAccess": true | обращение по индексу может вернуть undefined — и это видно в типе |
| "exactOptionalPropertyTypes": true | поля нет и поле равно undefined — разные вещи |
| // "outDir": "./dist" | закомментировано: пока молчит, .js ложится рядом с .ts |
Разбирать каждую строчку сейчас незачем — настройке проекта отведена своя глава в конце части. Одна строка, впрочем, стоит того, чтобы потрогать её сразу: strict. Вот параметр без аннотации.
TS7006 появилась не сама по себе — её включил strict. Без него компилятор молча приписал бы параметру тип any, и вторая строка упала бы в рантайме без единого предупреждения. В песочнице учебника strict включён всегда — ровно как в свежем tsconfig.json.
Самое коварное в tsconfig.json — что его легко не заметить. Если передать tsc имя файла прямо в командной строке, конфиг не читается вообще: ни strict, ни target, ничего. Годами это происходило молча, и люди подолгу не понимали, почему строгость «не работает».
В TypeScript 6 компилятор наконец говорит об этом вслух: error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.. Правило простое — зовите npx tsc без имён файлов. Что компилировать, он выяснит сам: по полю include, а если его нет, то по всему дереву от корня проекта.
Классический маршрут в две команды: npx tsc превращает src/app.ts в dist/app.js, а node dist/app.js его выполняет. Компилятор делает здесь две работы — проверяет типы и выдаёт JavaScript. Независимые в самом буквальном смысле: вторая не отменяется из-за первой.
Ошибка TS2322 — и всё равно 300300. Строка приклеилась к числу, потому что reduce ничего не знает о типах: до запуска доехал обычный JavaScript со сложением через +, а это тот самый оператор из главы про преобразование типов, который при виде строки бросает арифметику и начинает склеивать.
В терминале происходит ровно то же: tsc напечатает ошибку, вернёт код выхода 2 — и всё-таки положит app.js на диск. Это осознанное решение, а не недосмотр: типы не должны мешать вам запустить наполовину дописанный код. Нужно обратное — есть настройка "noEmitOnError": true, с ней при ошибке файлы не пишутся.
Код выхода важнее файла: по нему CI понимает, что сборка провалилась. Глазами в терминал на сервере никто не смотрит.
Пересобирать руками после каждого сохранения незачем: npx tsc --watch держит компилятор запущенным и перепроверяет только то, что изменилось. Это же делает ваш редактор — в нём внутри крутится тот же компилятор, только без выдачи файлов.
С версии 22.6 Node умеет открывать .ts-файлы сам — сначала под флагом --experimental-strip-types, а начиная с 22.18 и 23.6 без всяких флагов. node app.ts просто работает. Слово в названии флага важнее, чем кажется: strip — «содрать». Node не компилирует ваш файл, а вычёркивает из него всё, что относится к типам, и отдаёт движку то, что осталось.
Вычёркивает буквально: на месте аннотаций остаются пробелы. Поэтому номера строк и колонок в стеке вызовов совпадают с вашим исходником до символа, и никакие карты исходников для этого не нужны.
А вот проверки типов нет никакой. Совсем. Файл с десятком ошибок запустится так же бодро, как безупречный. Зато на таком запуске хорошо видно, что от типов остаётся после стирания.
role — значение, оно доживает до запуска. Role — только тип, и после стирания на его месте нет ничего: не undefined, не пустой объект, а вообще ничего — отсюда ReferenceError. Компилятор, впрочем, предупреждал заранее, диагностикой TS2693.Из правила «типы стираются» есть исключения — куски синтаксиса TypeScript, которые стереть нельзя, потому что они порождают настоящий работающий код. Самый известный — enum.
После компиляции Status — живой объект с обратным отображением: по имени достаётся число, по числу — имя. Такое не вычёркивается, такое генерируется. Поэтому node app.ts на этой строке останавливается с ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX: TypeScript enum is not supported in strip-only mode. Тот же отказ ждёт namespace и параметры-свойства вроде constructor(private name: string) — полный список разбирается в главе про классы.
Чтобы такой синтаксис не пробрался в проект незаметно, у компилятора есть флаг erasableSyntaxOnly: с ним enum и namespace становятся ошибкой TS1294 ещё на проверке.
Ещё пару лет назад запустить .ts напрямую можно было только через сторонний загрузчик, и они делились ровно по тому же признаку. ts-node честно звал компилятор и проверял типы на лету — потому и был медленным. tsx и загрузчик на swc пошли другим путём: стирают и не проверяют ничего, зато мгновенно. Node в итоге встроил у себя именно второй подход.
Оба пакета никуда не делись: они умеют то, чего встроенное стирание пока не умеет, — например, понимать псевдонимы путей из paths в конфиге. Но для «запустить один файл и посмотреть» ставить их больше не нужно.
Третий способ вы встретите чаще двух первых, хотя запускать .ts руками при этом не будете ни разу. Vite, Next, esbuild принимают .ts на вход. Сам файл внутри них переводит компилятор файла — Oxc в Vite, SWC в Next, а esbuild умеет и перевести файл, и собрать бандл. И все они делают с типами ровно то же, что Node: стирают. Это не недоделка, а решение о скорости: стирание — один проход по файлу, а проверка требует построить всю программу целиком, со всеми импортами, и обойти её.
| Способ | Проверяет типы | Что делает с кодом |
|---|---|---|
| npx tsc | да | проверяет и выдаёт .js |
| npx tsc --noEmit | да | только проверяет, файлов не пишет |
| node app.ts | нет | стирает типы и сразу выполняет |
| компилятор файла: Oxc в Vite, SWC в Next.js, esbuild | нет | стирает типы в одном файле |
| сборщик: Rolldown в Vite, Turbopack в Next.js, webpack | нет | зовёт компилятор файла и склеивает результат |
Отсюда практический вывод, ради которого написана вся статья: если сборкой занимается не tsc, проверка типов становится отдельной командой. Её кладут в package.json под именем вроде typecheck — это tsc --noEmit — и обязательно зовут в CI рядом с тестами. Иначе типы превращаются в комментарии: написаны красиво, не читает никто.
И последнее, о чём стоит договориться сразу. Даже tsc — единственный из трёх, кто действительно проверяет, — проверяет текст программы, а не данные, которые в неё придут.
Компилятор промолчал: as — не проверка, а обещание. Вы сказали «поверь, здесь такой тип», он поверил. Дальше типы стёрлись, и отвечать за них в рантайме некому — получите обычный TypeError, из тех, что вы ловили ещё в части I. Где проходит граница между «компилятор знает» и «мы надеемся» — тема отдельной главы ближе к концу части, а разбор самого as — в статье «as и @ts-expect-error: когда вы врёте компилятору». Пока запомните симптом: там, где написано as, проверок больше нет.
- Компилятор — обычный пакет проекта:
npm install -D typescript, запуск черезnpx tscили скрипт вpackage.json; глобальная установка разводит вашу версию с версией проекта. tsc --initсоздаётtsconfig.json, где строгость уже включена. Имя файла в командной строке отменяет чтение конфига — в TypeScript 6 это ошибкаTS5112.npx tscпроверяет типы и выдаёт JavaScript, причём выдаёт даже при ошибках: провал видно по коду выхода, а не по отсутствию файлов.node app.tsне компилирует, а стирает: аннотации заменяются пробелами, проверки нет вовсе, аenum,namespaceи параметры-свойства такой запуск не переживают.- Компиляторы файла внутри сборщиков — Oxc в Vite, SWC в Next, esbuild — поступают с типами как Node. Поэтому проверка живёт отдельной командой
tsc --noEmitв скриптах и в CI. - Ни один способ запуска не проверяет данные во время работы: после стирания за типы не отвечает никто, а
as— это обещание, а не проверка.
1/4В файле app.ts есть ошибка типов. Вы запускаете node app.ts. Что произойдёт?
2/4npx tsc нашёл в проекте ошибку типов. Появится ли на диске собранный .js?
3/4В корне проекта лежит tsconfig.json со strict: true. Вы выполняете npx tsc app.ts. Что будет с настройками из конфига?
4/4Почему node app.ts спотыкается на enum, хотя обычные аннотации типов стирает без вопросов?
Три запуска одного файла
Перед вами price.ts. Ответьте на три вопроса до того, как нажмёте Run: что скажет npx tsc --noEmit, появится ли price.js после npx tsc и что напечатает node price.ts.
npx tsc --noEmit найдёт одну ошибку — TS2345, аргумент типа string нельзя назначить параметру типа number. Код выхода будет ненулевым, файлов на диске не появится: об этом и просит --noEmit.
Обычный npx tsc напечатает ту же ошибку и тоже завершится с ненулевым кодом — и всё равно создаст price.js. Ошибка типов выдачу не отменяет.
node price.ts промолчит и напечатает 120.00: типы стёрты, а умножение строки на число в JavaScript отлично работает — "100" * 1.2 даёт 120. Три запуска, три разных ответа на один и тот же файл, и только первые два вообще смотрели на типы.
Файл, который Node не откроет
Код ниже спокойно проходит компилятор, но node app.ts падает на первой же строке с ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. Перепишите его так, чтобы поведение сохранилось, а Node смог запустить файл напрямую.
Мешает enum: он порождает объект, которого нет в исходнике, а режим стирания умеет только вычёркивать. Замена — обычный объект: в собранном коде на его месте всё равно был бы объект.
Это не только уловка ради Node: у объекта с as const получаются точнее типы, чем у enum, и в главе про классы этому посвящена отдельная статья. А чтобы enum не проник в проект незаметно, включают erasableSyntaxOnly — тогда о нём скажет ещё компилятор.
Зелёная сборка и красный прод
В проекте на Vite сборка проходит, бандл уезжает на прод — и там всплывает ошибка, которую компилятор нашёл бы за секунду. Почему сборщик её не заметил и какую одну команду надо добавить в скрипты и в CI?
Vite (а точнее компилятор файла у него внутри — сейчас это Oxc, раньше был esbuild) типы не проверяет — он их стирает, ровно как node app.ts. Зелёная сборка означает только то, что код удалось разобрать и склеить; про типы она не говорит ничего.
Дальше npm run typecheck ставят в CI рядом с тестами. Важен именно код выхода: при ошибке tsc вернёт не ноль, и шаг покраснеет. А --noEmit гарантирует, что проверка не оставит после себя ни одного лишнего файла рядом с исходниками.