Введение

Начнем со знакомства. Меня зовут Дмитрий, я Frontend разработчик с опытом 8 лет, в данный момент работаю с React и TypeScript. Отсюда вытекает, что мне нравится ESM синтаксис, ну и опыта с Node.JS у меня не так много, в основном пишу небольшие скрипты, иногда пет-проекты. Поэтому статью я позиционирую в основном для новичков, для тех, кто, как и я пишут периодически небольшие скрипты на Node. В самой статье мы разберемся, как переписать код с JavaScript на TypeScript с минимальными изменениями структуре скриптов.

Скрипты, большие и малые

Часть из разработчиков, как и я, пишут небольшие вспомогательные скрипты для различных целей, например распарсить, скачать или автоматизировать что-то. Для этого я выбираю обычно самые простые и доступные мне инструменты, например JS (здесь и далее “JavaScript”) и Node. Но, как я не раз замечал, скрипты со временем усложняются, добавляется новый функционал, иногда даже добавляются тесты. Все бы ничего, вот только в JS нет явной типизации, что создает лично мне проблемы, учитывая, что скрипты я редактирую не часто и обычно знания об типах выветриваются из головы к моменту следующего редактирования. IDE, конечно же, пытается помочь с этим и вывести какие-то типы в подсказках, но на сложных структурах это чаще всего перестает работать и появляется пресловутый any. Наверняка найдутся и те, кому нравится работать с чистым JavaScript, но я к оным не отношусь и признаю, что мне легче работать с TS (здесь и далее “TypeScript”), нежели с JS на средних и больших проектах.

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

  • транспиляция из TS в JS;

  • запуск TS скриптов напрямую посредством ts-node.

Так как мои скрипты находятся в gitlab и исполняются в пайплайнах, то нужно либо в пайплайне транспилировать, либо сохранять сразу два файла, что субъективно мне не нравится.

Второй вариант казался более простым, устанавливаем ts-node и можно запускать ts напрямую, а уже ts-node сделает всё за нас. Такое решение мне показалось лучшим в моей трансформации. Но в этот момент я вспомнил, что где-то читал о том, что теперь Node умеет работать с TypeScript напрямую. И я сразу полез исследовать данный вопрос.

Node или ts-node

Поиск по интернету сразу привел меня на документацию Node [1]. В документации говорилось, что с версии 22.18.0 включен по умолчанию “type stripping” (ранее известный как --experimental-strip-types). По сути, это значит, что теперь можно запускать TS в Node. Но есть ограничение, можно использовать только “erasable typescript syntax”. Данный синтаксис, это все то, что просто исчезает после транспиляции. К примеру, это interface и type. А вот enum и namespace не являются уже таким синтаксисом, так как после транспиляции превращаются в JS.

Для примера возьмем следующий код:

enum Example {
    First = 'А нам всё равно. А нам всё равно.',
    Second = 'Массаракш!'
}

interface Meta {
    year: number;
    question: string;
    example: Example,
}

type MetaAlias = Meta;

function getQuestion(): MetaAlias {
    return {
        year: 1969,
        question: 'Угадай произведение',
        example: Example.First
    };
}

Как видим, тут есть синтаксис и тот, и другой. Посмотрим, что останется после транспиляции в JS:

var Example;
(function (Example) {
    Example\["First"\] = "А нам всё равно. А нам всё равно.";
    Example\["Second"\] = "Массаракш!";
})(Example || (Example = {}));
function getQuestion() {
    return {
        year: 1969,
        question: 'Угадай произведение',
        example: Example.First
    };
}

Мы видим, что в полученном коде остался только Enum, который превратился в переменную Example и вызов функции. От интерфейса с типом не осталась и следа в получившемся коде. Node с версией выше 22.18.0 (по умолчанию) просто “не смотрит” на типы TypeScript и запускает код, как будто это JS. Node не будет трансформировать enum, как это делает TypeScript компилятор. Поэтому существует данное ограничения в синтаксисе. С вопросом запуска мы разобрались.

Но теперь встает другой вопрос, как следить, чтобы такие типы не попали в скрипт. Для этого в Typescript 5.8.0 появился новый флаг “erasableSyntaxOnly”. Как раз таки он будет запрещать неразрешенный синтаксис и сигнализировать ошибками, в случае его использования.

В документации Node рекомендуют использовать следующий конфиг для TS:

{
  "compilerOptions": {
     "noEmit": true, // Optional - see note below
     "target": "esnext",
     "module": "nodenext",
     "rewriteRelativeImportExtensions": true,
     "erasableSyntaxOnly": true,
     "verbatimModuleSyntax": true
  }
} 

Данный конфиг будет использоваться только TypeScript компилятором. Сейчас же Node просто игнорирует его. Для запуска TS кода он не является необходимым, он нужен только если вы хотите проверять код посредством компилятора.

Разобравшись с типами и новой версией дело остается за малым. Алгоритм перехода с JavaScript на TypeScript следующий:

  • обновляем Node до версии 22.18.0 и выше;

  • устанавливаем TypeScript, копируем конфиг и добавляем проверку типов;

  • меняем расширение файла на .ts/.mts.

Теперь скрипт должен запускаться, а мы можем радоваться жизни, если, конечно, TS не начал ругаться, в таком случае, нужно исправить ошибки.

Также стоит упомянуть, если вы пишите код, который импортирует типы, то нужно использовать следующий синтаксис:

import type { SomeThing } from "./some-module.js";

Node его просто пропустит и фактически никакого импорта не будет. Если вы случайно импортируете без type, то TS подсветит вам ошибку и предложит поменять импорт.

Но, к сожалению, не везде TypeScript может определить, что импортируется тип и соответственно ошибка будет уже в Runtime, когда при попытке импортировать сущность окажется, что её не существует.

К примеру, в моём случае был импортирован Dirent класс из node:fs и произошла ошибка в Runtime. Как выяснилось node:fs не экспортирует Dirent, а экспортируется только тип Dirent из d.ts, который объявлен как Class. Соответственно TS не замечает ошибки в следующем коде.

import { Dirent } from 'fs';

Как мы уже поняли, в данном случае нужно импортировать type и ошибка пропадет.

import type { Dirent } from 'fs';

Заключение

Я рассказал про новые возможности Node, про свой личный опыт с переездом с JS на TS. Теперь и вы можете переезжать на TS, либо сразу использовать TS в своих пет-проектах/скриптах.

А те, кому все-таки нужны namespace, enum и другие “неисчезающие” сущности могут исследовать флаг “--experimental-transform-types”, пока он является экспериментальным, что видно из названия. Возможно, в скором времени произойдут изменения и функционал стабилизируется. Ну или можно пользоваться ts-node.

Те, кто хочет посмотреть, как влияют различные флаги TS конфига и во что преобразуется код, могут это сделать на TS Playground [2]. Там есть как примеры кода, так и возможность изучить получившийся JS код.

Источники

  1. https://nodejs.org/docs/latest-v22.x/api/typescript.html#type-stripping

  2. https://www.typescriptlang.org/play/

Комментарии (0)


  1. zartdinov
    22.09.2025 06:11

    Посмотрите еще Bun.js.

    Довольно удобно для скриптов.


    1. amiduts Автор
      22.09.2025 06:11

      Да, когда-нибудь и его гляну, сейчас у меня почти всё на node.js. Особо не щупал ни на Bun, ни на Deno, но попробовать думаю стоит)


    1. konstantin_smirnov
      22.09.2025 06:11

      Для меня эта тулза тоже стала настоящим открытием в плане серверного js, однозначно хороший совет


  1. pavlushk0
    22.09.2025 06:11

    Есть ещё --experimental-strip-types. В 24 уже даже не experimental а по дефолту.


    1. amiduts Автор
      22.09.2025 06:11

      Возможно стоило про это более явно написать, но именно про это и рассказывается в статье) Теперь в 22.18 LTS и 24 Active по умолчанию включен Strip Types, что и позволило запускать TS без флагов
      Спасибо)


  1. itGuevara
    22.09.2025 06:11

    Есть ли полный список всего, что транслируется в JS?


    1. amiduts Автор
      22.09.2025 06:11

      Да, если я правильно понял вопрос, в официальной доке описан так. Что подобное этому конструкции не будут работать:

      • enum declarations

      • namespaces and modules with runtime code

      • parameter properties in classes

      • Non-ECMAScript import = and export = assignments

      Ссылка - https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-8.html#:~:text=module nodenext.-,The,Option,-Recently%2C Node.js (надеюсь она сработает, не часто копирую ссылку на выделенное)
      Ну если хочется более глубоко изучить, то можно посмотреть https://github.com/microsoft/TypeScript/pull/61011, там в изменениях, можно увидеть как раз проверки на этот синтаксис, которые выкидывают ошибку в случае нахождения оного