Нативный TypeScript в Node.js

• node, typescript

Введение

За последнее время были как хорошие новости про поддержку TypeScript, так и вызывающие недоумение. Также во всём этом немало странностей и подводных камней.

Рассмотрим тему с разных сторон и попробуем понять, какой статус у выполнения TypeScript в Node.js на 2026й год.

Вернёмся немного назад

В версии 22.6.0 в Node.js за флагом --experimental-strip-types появилась возможность исполнять файлы на TypeScript без каких-либо внешних инструментов:

node --experimental-strip-types script.ts

А с версии 23.6.0 флаг включён по умолчанию (его можно выключить обратным флагом). С версии 24.12.0+ / 25.2.0 функционал считается стабильным.

Фронтенд-разработчики и раньше писали не только продуктовый код на TypeScript, но и конфиги, сборочные скрипты, утилиты. Обычно для такого использовали такие инструменты, как ts-node. А с новым флагом всё это вышло на новый уровень: люди поверили, что счастье близко, и везде можно будет использовать TypeScript также легко, как и JavaScript.

Что не работает с таким подходом?

Не работают конструкции, которые нельзя просто удалить из кода, те, которые влияют на рантайм.

enum

Номер 1 в списке ввиду популярности.

enum Color {
    Red,
    Blue,
    Green
}

Такая конструкция в режиме “удаления типов” приведёт к ошибке ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX.

Кто-то скажет, что подобный способ написания кода очень полезен, более читабелен и так далее. Я бы сказал, что в ряде случаев вместо него довольно просто использовать union-типы:

type Color = 'Red' | 'Blue' | 'Green`;

В каких-то случаях этого хватит. Но мотивацию защитников enum я понимаю. Кроме того, от enum не уйти, если идёт работа с внешними инструментами, например, с протобуфами.

namespace

namespace ExternalAPI {
    export function callSmth(): void {};
}

namespace тоже популярны, и уже достаточно давно есть рекомендация не использовать их.

Команда, работающая над TypeScript, давно высказалась о своей позиции: рантаймовые конструкции, которые порождает язык, были ошибкой ранних версий, и такие возможности больше не добавляются.

Что остаётся? Использовать объекты:

const ExternalAPI = {
    callSmth(): void {
    }
}

И namespace всё ещё доступы, если используются только для типов:

namespace TypeOnly {
   export type A = string;
}

Импорты типов без type

Наверно, сейчас так пишут уже редко, но всё же.

import { Runtime, Types } from './smth';
 
// вместо
 
import { Runtime, type Types } from './smth';

Подобная конструкция приводит к тому, что Node.js (и другие инструменты) не понимают, что импортируется в данном месте. Конечно, можно сказать: да ведь всё понятно, если используется, как тип, то это тип. Но по факту это не спасает от реэкспортов (типы выпиливаются из файлов изолированно), а во-вторых, функционал удаления типов работает сильно проще и не отслеживает, откуда что импортируется.

Кроме того, указание type помогает и процессу сборки: если импорт помечен подобным образом, то он не нужен рантайму, и в части случаев его можно опустить (и даже не парсить соседний файл!). На самом деле, вот такая конструкция ещё лучше:

// Лучше
import type { Types } from './smth';
 
// Чем
import { type Types } from './smth';

Из-за возможного наличия сайд-эффектов в smth, второй импорт не всегда можно просто так удалить, а первый - обычно можно. Если интересна тема, почитайте про verbatimModuleSyntax.

Объявления полей класса через параметры конструктора

Вот такая конструкция тоже влияет на рантайм:

class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
}

Понятное дело, этот код легко переписывается так, чтобы было обычное присвоение полей, но такой подход всё равно встречается.

Декораторы

class Point {
    @loggable
    len() {
    }
}

Декораторы – зыбкая почва уже долгое время. Учитывая, что они всё ещё не реализованы нормально ни в браузерах, ни в Node.js, ни один вариант декоторов из TypeScript в чистой ноде не заработает.

Ждём новостей по этому долгострою? Одна из самых долгих в разработке фич, и одна из самых популярных.

Поддержка со стороны TypeScript

В TypeScript 5.8 появился флаг erasableSyntaxOnly, который позволяет ограничить свой код “сабсетом” TypeScript, чтобы итоговый код мог выполняться Node.js и другими инструментами без полноценной поддержки TS.

Кстати, авторы рекомендуют включить ещё одну знакомую нам опцию, verbatimModuleSyntax.

Выход есть?

В версии Node.js 22.7.0 появился второй флаг, --experimental-transform-types. Он позволяет преобразовывать рантаймовый синтаксис TypeScript в обычный JS, тем самым в Node.js теперь можно запускать ещё больше TypeScript кода (скорее всего, почти весь, без учёта полей из tsconfig по типу paths).

А дальше случается интересное. В версии 26.0.0 флаг… Удалили. Не включили по умолчанию, а именно удалили. Тем самым в актуальной версии Node.js есть только включённый по дефолту strip-types, который позволяет выполнить ограниченный синтаксис TypeScript.

Почему? Есть несколько причин:

  • В JavaScript могут появиться новые возможности, например, enum, и тогда выпиливать их станет неправильно
  • Синтаксис TypeScript меняется, и это за пределами команды Node.js
  • Сложно поддерживать SemVer в таких условиях

Чем это плохо для обычных разработчиков?

Тем, что больше нельзя надеяться на полноценный запуск TypeScript кода в Node.js (а тем, кто уже успел на это положиться, нужно будет отказываться). А любые дополнительные прослойки (будь то amaro, хуки на transform в Node.js или что угодно ещё) исторически себя показали не очень хорошо.

Какие варианты остаются?

1) Ограничить себя erasableSyntaxOnly

Плохо тем, что нужно явно убирать часть возможностей TypeScript (а ещё редко код может придти извне нашей зоны ответственности).

2) Использовать дополнительные инструменты

Увеличивает стоимость поддержки, ухудшает скорость работы.

Что же дальше? Видимо, время покажет. А пока предлагаю посмотреть статус поддержки нативного TypeScript в различных инструментах.

Поддержка нативного TypeScript в инструментах

Vite & Vitest

Поддержка конфигов TypeScript с первых версий. Просто работает – не нужны дополнительные плагины, шаги, другие команды, ничего.

Под капотом использует vite-node, инструмент, который можно использовать для запуска своего кода на TypeScript (полноценного, не strip-types).

Кроме того, процесс сборки поддерживает обработку TypeScript также, из коробки.

Rolldown

В Rolldown ситуация аналогична Vite: поддержка конфигов есть из коробки, тайпинги тоже, собирать файлы на TS умеет.

TypeScript и тут является языком с отличной поддержкой без дополнительных усилий.

Webpack

В версии 5.107.0, которая вышла два дня назад, появилась поддержка конфигов вебпака на TypeScript из коробки! Учитывая, что в пакете есть встроенные тайпинги, можно очень легко начать писать конфиги на TypeScript без дополнительных зависимостей.

И это не всё. Добавилась экспериментальная поддержка трансформации TypeScript кода силами Node.js, т..е только “удаляемый” синтаксис типов без рантайма. Конечно, не так удобно, как в Vite, да и к тому же, за отдельным экспериментальным флагом, но уже большой шаг вперёд. А при желании можно всё также использовать свой лоадер.

Rspack

В Rspack поддержка TypeScript также есть из коробки, и для конфигов работает следующая логика:

  1. Если конфиг на JS, то используется нативное выполнение
  2. Если есть поддержка TypeScript в Node.js, то используется она. Если она падает – идёт фолбек в jiti
  3. Если поддержки нет, то сразу используется jiti

Сборка кода на TypeScript тоже встроенная, однако требуется конфигурация, чтобы это заработало.

Jest

Jest выглядит… Подозрительно, скажем так)

С одной стороны, он поддерживает конфиги в TypeScript файлах. И делает это в том числе с помощью нативной поддержки в Node.js. С другой – не видно документации, которая бы про это рассказывала. Официальная документация предлагает использовать ts-node.

Поддержки тестов на TypeScript из коробки нет (хотя они и находятся встроенными масками и даже пытаются выполниться, как JS).

Но определённая тенденция видна: поддержка конфигов есть, тайпинги для конфигов теперь приезжают прямо в пакете jest (а не в другом, @types/jest). Сможет ли Jest дойти до встроенной поддержки тестов на TypeScript? Хочется верить, что да, ведь у Webpack получилось сделать свою загрузку кода на нативном механизме.

Prettier

Prettier из коробки поддерживает конфиги в TypeScript. Дополнительных настроек не требуется (если версия Node.js достаточно свежая). Тайпинги в составе пакета тоже есть.

ESLint

С ESLint ситуация интересная. С одной стороны, поддержка конфига на TypeScript через нативную поддержку в Node.js есть. С другой – это требует специального флага, unstable_native_nodejs_ts_config.

Если флаг не передать, ESLint использует уже знакомый нам jiti. Даже в том случае, если поддержка в Node.js есть… Соответствующий issue закрыт. Хочется верить, что авторы пересмотрят подходит и сделают возможность использовать конфиги на TypeScript без дополнительных флагов и настройки.

Итого

Поддержка запуска TypeScript кода в Node.js путём удаления типов сдвинула экосистему. Множество инструментов на неё перешли или она просто внезапно заработала. Сборка и работа с TypeScript всё чаще работает на том же уровне, что и JavaScript-код.

Всё больше пакетов содержат в себе тайпинги для TypeScript, и дополнительные зависимости больше не нужно устанавливать.

Однако полноценная поддержка всех возможностей TypeScript в Node.js больше не планируется. Как с этим жить и что будет дальше – вопрос открытый. Я же пока для себя хочу остановиться на варианте с erasableSyntaxOnly и использовать TypeScript везде, где получится.

Не раз и не два я встречал код на JavaScript, который содержал поля с неправильными именами, кривые конфиги и так далее… От всего этого TypeScript бы мог спасти.

Обсудить в Telegram

Почитать ещё посты

  • Обзор TypeScript 6
  • Type Branding и Type Flavoring
  • Ищем баланс с помощью flex-wrap: balance
  • Калькулятор фолбека для corner-shape
  • Вышел Rolldown 1.0