Установка и три способа запустить .ts

компилятор≈ 9 минут

Здесь будет встроен видео-разбор темы (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 — надстройка, а не другой язык.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор промолчал — значит, придраться было не к чему. И ещё одно: диагностики, которые вы будете видеть под примерами, не нарисованы в тексте — их выдаёт настоящий компилятор, и по-русски он говорит потому, что везёт переводы с собой. Только версия у него не та, что вы сейчас поставили. Сегодня 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. Вот параметр без аннотации.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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. Независимые в самом буквальном смысле: вторая не отменяется из-за первой.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ошибка 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 не компилирует ваш файл, а вычёркивает из него всё, что относится к типам, и отдаёт движку то, что осталось.

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

А вот проверки типов нет никакой. Совсем. Файл с десятком ошибок запустится так же бодро, как безупречный. Зато на таком запуске хорошо видно, что от типов остаётся после стирания.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера
разбор вывода
role — значение, оно доживает до запуска. Role — только тип, и после стирания на его месте нет ничего: не undefined, не пустой объект, а вообще ничего — отсюда ReferenceError. Компилятор, впрочем, предупреждал заранее, диагностикой TS2693.

Из правила «типы стираются» есть исключения — куски синтаксиса TypeScript, которые стереть нельзя, потому что они порождают настоящий работающий код. Самый известный — enum.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

После компиляции 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 — единственный из трёх, кто действительно проверяет, — проверяет текст программы, а не данные, которые в неё придут.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор промолчал: 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.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

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 смог запустить файл напрямую.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Мешает enum: он порождает объект, которого нет в исходнике, а режим стирания умеет только вычёркивать. Замена — обычный объект: в собранном коде на его месте всё равно был бы объект.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Это не только уловка ради Node: у объекта с as const получаются точнее типы, чем у enum, и в главе про классы этому посвящена отдельная статья. А чтобы enum не проник в проект незаметно, включают erasableSyntaxOnly — тогда о нём скажет ещё компилятор.

Зелёная сборка и красный прод

В проекте на Vite сборка проходит, бандл уезжает на прод — и там всплывает ошибка, которую компилятор нашёл бы за секунду. Почему сборщик её не заметил и какую одну команду надо добавить в скрипты и в CI?

Vite (а точнее компилятор файла у него внутри — сейчас это Oxc, раньше был esbuild) типы не проверяет — он их стирает, ровно как node app.ts. Зелёная сборка означает только то, что код удалось разобрать и склеить; про типы она не говорит ничего.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Дальше npm run typecheck ставят в CI рядом с тестами. Важен именно код выхода: при ошибке tsc вернёт не ноль, и шаг покраснеет. А --noEmit гарантирует, что проверка не оставит после себя ни одного лишнего файла рядом с исходниками.