Сегодня команда TypeScript с радостью объявляет о выходе Release Candidate (RC) TypeScript 6.0.

Чтобы начать пользоваться RC-версией, установите её через npm:

npm install -D typescript@rc

TypeScript 6.0 — особенный релиз: он задуман как последняя версия на основе текущей JavaScript-кодовой базы.

Как команда объявила ещё в прошлом году, сейчас идёт работа над новой реализацией компилятора TypeScript и language service на Go. Она использует преимущества нативного кода и многопоточности с общей памятью. Именно эта новая кодовая база станет фундаментом TypeScript 7.0 и следующих версий.

TypeScript 6.0 — это ближайший предшественник этого релиза и во многом мост между TypeScript 5.9 и TypeScript 7.0. Поэтому значительная часть изменений в TypeScript 6.0 нужна для того, чтобы выровнять поведение и подготовить экосистему к переходу на TypeScript 7.0.

При этом в релизе есть и новые возможности, которые не сводятся только к подготовке к миграции.

Ниже — сначала ключевые новинки этой версии, а затем более подробный разбор того, что изменится в TypeScript 7.0 и как к этому подготовиться.

Что изменилось со времени Beta?

После выхода бета-версии TypeScript 6.0 появилось несколько заметных изменений — в основном для выравнивания поведения с TypeScript 7.0.

Одно из них касается проверки типов у function expression в generic-вызовах, особенно внутри generic JSX-выражений. Это позволит находить больше ошибок в уже существующем коде, хотя в некоторых местах теперь может понадобиться явно указать аргумент типа.

Кроме того, команда расширила депрекацию синтаксиса import assertion (import ... assert {...}) и на вызовы import() вроде import(..., { assert: {...} }).

Наконец, были обновлены DOM-типы в соответствии с последними веб-стандартами, включая некоторые изменения, связанные с API Temporal.

Меньше контекстной чувствительности у функций без this

Когда параметры функции не имеют явно указанных типов, TypeScript обычно умеет вывести их либо из ожидаемого типа, либо даже из других аргументов в том же вызове функции.

declare function callIt<T>(obj: {
    produce: (x: number) => T,
    consume: (y: T) => void,
}): void;
 
// Works, no issues.
callIt({
    produce: (x: number) => x * 2,
    consume: y => y.toFixed(),
});
 
// Works, no issues even though the order of the properties is flipped.
callIt({
    consume: y => y.toFixed(),
    produce: (x: number) => x * 2,
});

Здесь TypeScript умеет вывести тип y в функции consume, опираясь на выведенный тип T из функции produce, и порядок свойств при этом не важен.

Но что будет, если записать эти функции не стрелками, а через method syntax?

declare function callIt<T>(obj: {
    produce: (x: number) => T,
    consume: (y: T) => void,
}): void;
 
// Works fine, `x` is inferred to be a number.
callIt({
    produce(x: number) { return x * 2; },
    consume(y) { return y.toFixed(); },
});
 
callIt({
    consume(y) { return y.toFixed(); },
    //                  ~
    // error: 'y' is of type 'unknown'.
 
    produce(x: number) { return x * 2; },
});

Как ни странно, второй вызов callIt приводит к ошибке: TypeScript не может вывести тип y в методе consume.

Причина в том, что когда TypeScript ищет кандидатов для типа T, он сначала пропускает функции, у которых параметры не имеют явных типов. Это делается потому, что таким функциям самим может понадобиться уже выведенный T, чтобы корректно пройти проверку типов. В нашем случае, чтобы проверить consume, системе типов уже нужно знать T.

Такие функции называются contextually sensitive functions — то есть функции, у которых есть параметры без явно указанных типов. Позже система всё равно должна будет вывести типы этих параметров, но это конфликтует с самой логикой вывода generic-типов: обе стороны как бы «тянут» типы в разные направления.

function callFunc<T>(callback: (x: T) => void, value: T) {
    return callback(value);
}
 
callFunc(x => x.toFixed(), 42);
//       ^
// We need to figure out the type of `x` here,
// but we also need to figure out the type of `T` to check the callback.

Чтобы решить эту проблему, TypeScript при выводе аргументов типа сначала пропускает contextually sensitive functions и пытается вывести типы по другим аргументам. Если этого недостаточно, вывод продолжается по остальным непроверенным аргументам слева направо.

В примере выше TypeScript сначала пропустит callback при выводе T, затем посмотрит на второй аргумент 42 и поймёт, что T — это number. После этого он вернётся к callback и уже сможет контекстно вывести, что x тоже имеет тип number.

Так что же происходило в наших первых примерах?

// Arrow syntax - no errors.
callIt({
    consume: y => y.toFixed(),
    produce: (x: number) => x * 2,
});
 
// Method syntax - errors!
callIt({
    consume(y) { return y.toFixed(); },
    //                  ~
    // error: 'y' is of type 'unknown'.
 
    produce(x: number) { return x * 2; },
});

В обоих случаях produce получает функцию с явно типизированным параметром x. Казалось бы, поведение должно быть одинаковым.

Но тут есть тонкость: у большинства обычных функций (включая методы) есть неявный параметр this, а у стрелочных функций — нет. Любое использование this может потребовать «потянуть» тип T в обратную сторону. Например, чтобы понять тип содержащего объектного литерала, может потребоваться знать тип consume, а он использует T.

Но в нашем примере this вообще не используется.

Именно это TypeScript 6.0 теперь учитывает. Если this внутри функции фактически не используется, такая функция больше не считается contextually sensitive. Это означает, что при выводе типов она получает более высокий приоритет, и все приведённые выше примеры теперь работают корректно.

Это изменение появилось благодаря работе Mateusz Burzyński.

Subpath Imports с префиксом #/

Когда в Node.js появилась поддержка модулей, там же появился механизм subpath imports.

По сути это поле imports, которое позволяет пакету задавать внутренние алиасы для модулей внутри самого пакета.

{
    "name": "my-package",
    "type": "module",
    "imports": {
        "#root": "./dist/index.js",
        "#root/*": "./dist/*"
    }
}

Это позволяет модулям внутри my-package импортировать из #root вместо длинных относительных путей вроде ../../index.js. То есть можно писать так:

import * as utils from "#root/utils.js";

вместо такого варианта:

import * as utils from "../../utils.js";

У этой возможности был один раздражающий момент: после # разработчикам всегда приходилось писать что-то ещё. В примере выше используется root, но на деле это лишний сегмент, потому что никакой особой директории, кроме ./dist/, тут нет.

Те, кто привык к bundler’ам, давно используют алиасы для избавления от длинных относительных путей. Распространённая конвенция — префикс @/. Но subpath imports до недавнего времени вообще не позволяли начинать путь с #/, из-за чего это вызывало путаницу у разработчиков, пытавшихся внедрить их в свои проекты.

Недавно Node.js добавил поддержку subpath imports, начинающихся с #/. Благодаря этому пакет теперь может использовать простой префикс #/ без лишнего сегмента.

{
    "name": "my-package",
    "type": "module",
    "imports": {
        "#": "./dist/index.js",
        "#/*": "./dist/*"
    }
}

Это поддерживается в новых релизах Node.js 20, и TypeScript теперь тоже поддерживает такой синтаксис для --moduleResolution со значениями:

  • node20
  • nodenext
  • bundler

Эта работа была сделана благодаря magic-akari.

Сочетание --moduleResolution bundler и --module commonjs

Раньше режим --moduleResolution bundler разрешалось использовать только вместе с --module esnext или --module preserve.

Однако после депрекации --moduleResolution node (он же --moduleResolution node10) именно такое сочетание во многих проектах становится самым удобным путём миграции.

Во многих случаях проектам логичнее планировать переход либо на:

  • --module preserve и --moduleResolution bundler
  • --module nodenext

в зависимости от типа проекта — например, это может быть веб-приложение с bundler’ом, приложение на Bun или приложение под Node.js.

Флаг --stableTypeOrdering

В рамках работы над нативной версией TypeScript команда добавила новый флаг --stableTypeOrdering, который должен помочь при миграции с 6.0 на 7.0.

Сейчас TypeScript присваивает типам внутренние ID в том порядке, в котором они встречаются, и затем использует эти ID для стабильной сортировки union-типов. Похожий процесс применяется и к свойствам.

Из-за этого порядок, в котором сущности объявлены в программе, может неожиданно влиять, например, на declaration emit.

Возьмём такой пример:

// Input: some-file.ts
export function foo(condition: boolean) {
    return condition ? 100 : 500;
}
 
// Output: some-file.d.ts
export declare function foo(condition: boolean): 100 | 500;
//                                               ^^^^^^^^^
//             Note the order of this union: 100, then 500.

Если добавить несвязанный const выше foo, вывод изменится:

// Input: some-file.ts
const x = 500;
export function foo(condition: boolean) {
    return condition ? 100 : 500;
}
 
// Output: some-file.d.ts
export declare function foo(condition: boolean): 500 | 100;
//                                               ^^^^^^^^^
//                           Note the change in order here.

Так происходит потому, что литеральный тип 500 получает более низкий внутренний ID, чем 100, так как был обработан раньше при анализе const x.

В очень редких случаях такая перестановка может даже приводить к тому, что ошибка то появляется, то исчезает в зависимости от порядка обработки программы. Но чаще всего это заметно либо в .d.ts-файлах, либо в том, как типы отображаются в редакторе.

Одно из главных архитектурных улучшений TypeScript 7 — параллельная проверка типов, которая сильно ускоряет анализ. Но у параллелизма есть проблема: когда разные type-checker’ы посещают узлы, типы и символы в разном порядке, внутренние ID становятся недетерминированными.

Это приводит к путаному недетерминированному выводу: два файла с одинаковым содержимым в одной и той же программе могут породить разные declaration-файлы или даже разные ошибки при анализе одного и того же кода.

Чтобы избежать этого, TypeScript 7.0 сортирует внутренние объекты (типы, символы и т.д.) по детерминированному алгоритму, основанному на содержимом самих объектов. Благодаря этому все checker’ы видят один и тот же порядок, независимо от того, когда и как эти объекты были созданы.

В результате TypeScript 7 всегда будет печатать 100 | 500, устраняя нестабильность порядка.

Это означает, что TypeScript 6 и 7 иногда показывают разный порядок. Почти всегда это безобидно, но если вы сравниваете результаты компиляции между версиями — например, проверяете .d.ts-вывод 6.0 и 7.0, — различия в порядке создают много шума.

Иногда перестановка может даже влиять на наличие или отсутствие ошибок типов, что ещё сильнее сбивает с толку.

Чтобы облегчить диагностику, в TypeScript 6.0 можно указать флаг:

--stableTypeOrdering

Он делает поведение TypeScript 6.0 в части порядка типов таким же, как в TypeScript 7.0, и уменьшает число различий между двумя кодовыми базами.

Но включать этот флаг постоянно не рекомендуется: он может ощутимо замедлить проверку типов — до 25% в зависимости от кодовой базы.

Если с --stableTypeOrdering вы сталкиваетесь с ошибкой типов, обычно причина в различиях вывода типов. Предыдущее поведение без этого флага просто случайно работало благодаря конкретному порядку типов в вашей программе.

Часто это исправляется явным указанием типа. Например, аргументом типа:

- someFunctionCall(/*...*/);
+ someFunctionCall<SomeExplicitType>(/*...*/);

или аннотацией переменной, которую вы передаёте в вызов:

- const someVariable = { /*... some complex object ...*/ };
+ const someVariable: SomeExplicitType = { /*... some complex object ...*/ };
 
someFunctionCall(someVariable);

Важно: этот флаг нужен именно для диагностики различий между TypeScript 6.0 и 7.0. Это не долгосрочная возможность, на которую стоит опираться постоянно.

Опция es2025 для target и lib

TypeScript 6.0 добавляет поддержку значения es2025 и для target, и для lib.

Хотя в самом ES2025 нет новых синтаксических возможностей JavaScript, новый target добавляет типы для встроенных API, например RegExp.escape, а также переносит некоторые объявления из esnext в es2025 — например:

  • Promise.try
  • методы Iterator
  • методы Set

Поддержка этого target появилась благодаря Kenta Moriuchi.

Новые типы для Temporal

Долгожданный proposal Temporal достиг стадии 3 и, как ожидается, в ближайшем будущем войдёт в JavaScript.

TypeScript 6.0 теперь включает встроенные типы для Temporal API, так что вы уже сейчас можете использовать его в TypeScript-коде через --target esnext или "lib": ["esnext"] (либо через более точечное temporal.esnext).

let yesterday = Temporal.Now.instant().subtract({
    hours: 24,
});
 
let tomorrow = Temporal.Now.instant().add({
    hours: 24,
});
 
console.log(`Yesterday: ${yesterday}`);
console.log(`Tomorrow: ${tomorrow}`);

Temporal уже доступен в нескольких runtime-средах, так что попробовать его можно довольно скоро. Документация по Temporal API уже есть на MDN, хотя местами она ещё может быть неполной.

Эта работа появилась благодаря участнику GitHub Renegade334.

Новые типы для upsert-методов (getOrInsert)

Обычный паттерн при работе с Map — проверить наличие ключа, а если его нет, записать и вернуть значение по умолчанию.

function processOptions(compilerOptions: Map<string, unknown>) {
    let strictValue: unknown;
    if (compilerOptions.has("strict")) {
        strictValue = compilerOptions.get("strict");
    }
    else {
        strictValue = true;
        compilerOptions.set("strict", strictValue);
    }
    // ...
}

Такой код довольно многословен.

Proposal upsert для ECMAScript недавно достиг стадии 4 и добавляет в Map и WeakMap два новых метода:

  • getOrInsert
  • getOrInsertComputed

Они уже добавлены в esnext-lib, так что в TypeScript 6.0 ими можно пользоваться сразу.

С getOrInsert предыдущий код превращается в:

function processOptions(compilerOptions: Map<string, unknown>) {
    let strictValue = compilerOptions.getOrInsert("strict", true);
    // ...
}

Метод getOrInsertComputed работает похоже, но подходит для случаев, когда значение по умолчанию дорого вычислять — например, оно требует большого числа вычислений, аллокаций или долгого синхронного I/O.

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

someMap.getOrInsertComputed("someKey", () => {
    return computeSomeExpensiveValue(/*...*/);
});

Этот callback также получает сам ключ как аргумент, что удобно в случаях, когда значение по умолчанию зависит от ключа.

someMap.getOrInsertComputed(someKey, computeSomeExpensiveDefaultValue);
 
function computeSomeExpensiveValue(key: string) {
    // ...
}

Эта возможность тоже добавлена благодаря Renegade334.

RegExp.escape

Когда нужно построить регулярное выражение для поиска буквальной строки, очень важно корректно экранировать специальные символы регулярок: *, +, ?, (, ) и так далее.

Proposal RegExp Escaping достиг стадии 4 и добавляет новую функцию RegExp.escape, которая делает это автоматически.

function matchWholeWord(word: string, text: string) {
    const escapedWord = RegExp.escape(word);
    const regex = new RegExp(`\\b${escapedWord}\\b`, "g");
    return text.match(regex);
}

RegExp.escape доступен в es2025, так что вы можете использовать его уже в TypeScript 6.0.

Библиотека dom теперь включает dom.iterable и dom.asynciterable

Опция lib в TypeScript позволяет указать, какие глобальные объявления есть в целевой среде выполнения.

Одно из значений — dom, которое соответствует веб-окружению, то есть браузерам и DOM API.

Раньше часть DOM API была вынесена в отдельные dom.iterable и dom.asynciterable для сред, где не было поддержки Iterable и AsyncIterable. Из-за этого для итерации по DOM-коллекциям вроде NodeList или HTMLCollection приходилось явно добавлять dom.iterable.

В TypeScript 6.0 содержимое lib.dom.iterable.d.ts и lib.dom.asynciterable.d.ts полностью включено в lib.dom.d.ts.

Вы всё ещё можете указывать dom.iterable и dom.asynciterable в массиве "lib" в конфиге, но теперь это просто пустые файлы.

// Before TypeScript 6.0, this required "lib": ["dom", "dom.iterable"]
// Now it works with just "lib": ["dom"]
for (const element of document.querySelectorAll("div")) {
    console.log(element.textContent);
}

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

Если раньше вы писали и dom, и dom.iterable, теперь можно спокойно оставить только dom.

Breaking changes и депрекации в TypeScript 6.0

TypeScript 6.0 — важный переходный релиз, который должен подготовить разработчиков к TypeScript 7.0, будущему нативному порту компилятора.

Хотя TypeScript 6.0 по-прежнему полностью совместим с вашими текущими знаниями о TypeScript и остаётся API-совместимым с TypeScript 5.9, в релизе есть ряд breaking changes и депрекаций, отражающих то, как изменилась экосистема JavaScript и куда она движется дальше.

За два года после TypeScript 5.0 в том, как разработчики пишут и поставляют JavaScript, многое изменилось:

  • практически все среды выполнения теперь evergreen
  • действительно устаревшие окружения уровня ES5 почти исчезли
  • bundler’ы и ESM стали основным выбором для новых проектов, хотя CommonJS по-прежнему важен
  • AMD и другие пользовательские модульные системы для браузеров встречаются намного реже, чем в 2012 году
  • почти любой пакет сейчас можно потреблять через какую-то модульную систему
  • UMD-пакеты ещё существуют, но почти не бывает нового кода, который доступен только как глобальная переменная
  • tsconfig.json стал фактически универсальным способом конфигурирования
  • спрос на более строгую типизацию продолжает расти
  • производительность TypeScript-сборки стала критически важной задачей

Поэтому и TypeScript 6.0, и 7.0 проектируются с учётом этих реалий.

В TypeScript 6.0 предупреждения о депрекациях можно игнорировать, если прописать в tsconfig:

{
  "ignoreDeprecations": "6.0"
}

Но важно понимать: в TypeScript 7.0 эти deprecated-опции уже не будут поддерживаться вовсе.

Некоторые из необходимых изменений можно выполнить автоматически через codemod или специальный инструмент. Например, экспериментальный ts5to6 умеет автоматически править baseUrl и rootDir по проекту.

На что обратить внимание сразу

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

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

  • Явно задать "types" в tsconfig, чаще всего как "types": ["node"].
  • Явно задать "rootDir": "./src", если раньше вы полагались на его автоматический вывод.

Простые изменения значений по умолчанию

  • strict теперь по умолчанию равен true
  • module теперь по умолчанию равен esnext
  • target теперь по умолчанию — актуальная версия ECMAScript, сейчас это es2025
  • noUncheckedSideEffectImports теперь по умолчанию равен true
  • libReplacement теперь по умолчанию равен false

Если эти новые defaults ломают ваш проект, вы можете явно прописать прежние значения в tsconfig.json.

rootDir теперь по умолчанию равен .

Если раньше TypeScript автоматически выводил rootDir, теперь он по умолчанию всегда равен директории, в которой лежит tsconfig.json.

Если вам нужен прежний сценарий с исходниками в src, задайте это явно:

  {
      "compilerOptions": {
          // ...
+         "rootDir": "./src"
      },
      "include": ["./src"]
  }

Если include смотрит за пределы директории с tsconfig.json, rootDir тоже нужно расширить:

  {
      "compilerOptions": {
          // ...
+         "rootDir": "../src"
      },
      "include": ["../src/**/*.tests.ts"]
  }

types теперь по умолчанию равно []

Раньше TypeScript по умолчанию подхватывал все пакеты из node_modules/@types. Теперь default — пустой массив.

В большинстве проектов вам понадобится явно перечислить нужные типы:

  {
      "compilerOptions": {
+         "types": ["node", "jest"]
      }
  }

Если нужно полностью вернуть старое поведение:

  {
      "compilerOptions": {
+         "types": ["*"]
      }
  }

Deprecated: target: es5

target: es5 устарел. Минимальным поддерживаемым target теперь становится ES2015.

Если вам всё ещё нужен ES5 output, придётся использовать внешний компилятор или постобработку результата TypeScript.

Deprecated: --downlevelIteration

Так как target: es5 устарел, --downlevelIteration тоже больше не имеет смысла и теперь считается deprecated.

Deprecated: --moduleResolution node

Режим node (node10) больше не отражает поведение современного Node.js.

Обычно переход должен быть на:

  • nodenext — для Node.js
  • bundler — для bundler’ов и Bun

Deprecated: amd, umd, systemjs, none

Следующие значения module больше не поддерживаются:

  • amd
  • umd
  • systemjs
  • none

Современная рекомендация — переходить на ESM и использовать bundler либо runtime с современной поддержкой модулей.

Deprecated: --baseUrl

baseUrl объявлен устаревшим и больше не будет использоваться как корень поиска при module resolution.

Если он использовался только как префикс для paths, просто перенесите этот префикс в сами пути:

  {
    "compilerOptions": {
      // ...
-     "baseUrl": "./src",
      "paths": {
-       "@app/*": ["app/*"],
-       "@lib/*": ["lib/*"]
+       "@app/*": ["./src/app/*"],
+       "@lib/*": ["./src/lib/*"]
      }
    }
  }

Если вы действительно использовали baseUrl как lookup root, это можно сохранить через wildcard mapping:

{
  "compilerOptions": {
    "paths": {
      "*": ["./src/*"],
      "@app/*": ["./src/app/*"],
      "@lib/*": ["./src/lib/*"]
    }
  }
}

Deprecated: --moduleResolution classic

classic удалён. Переходить нужно на nodenext или bundler.

Deprecated: --esModuleInterop false и --allowSyntheticDefaultImports false

Обе настройки больше нельзя выключить.

Теперь безопасное interop-поведение включено всегда.

// Before (with esModuleInterop: false)
import * as express from "express";
 
// After (with esModuleInterop always enabled)
import express from "express";

Deprecated: --alwaysStrict false

Весь код теперь считается работающим в strict mode JavaScript.

Если у вас есть «нестрогий» код, использующий зарезервированные слова как идентификаторы, или завязанный на старую семантику this, его придётся обновить.

Deprecated: outFile

--outFile удалён из TypeScript 6.0. Для склейки файлов теперь следует использовать внешний bundler.

Deprecated: устаревший синтаксис module для namespace

Старый синтаксис:

// ❌ Deprecated syntax - now an error
module Foo {
    export const bar = 10;
}

нужно заменить на:

// ✅ The correct syntax
namespace Foo {
    export const bar = 10;
}

При этом ambient module declarations по-прежнему поддерживаются:

// ✅ Still works perfectly
declare module "some-module" {
    export function doSomething(): void;
}

Deprecated: asserts в imports

Старый синтаксис import assertions больше не поддерживается:

// ❌ Deprecated syntax - now an error.
import blob from "./blahb.json" asserts { type: "json" }

Теперь нужно писать так:

// ✅ Works with the new import attributes syntax.
import blob from "./blahb.json" with { type: "json" }

Deprecated: no-default-lib directives

Директива /// <reference no-default-lib="true"/> больше не поддерживается.

Вместо неё используйте --noLib или --libReplacement.

Ошибка при указании файлов в командной строке, если рядом есть tsconfig.json

Теперь запуск tsc foo.ts в директории с tsconfig.json приводит к явной ошибке:

error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.

Если вы действительно хотите проигнорировать конфиг, используйте:

tsc --ignoreConfig foo.ts

Подготовка к TypeScript 7.0

TypeScript 6.0 — переходный релиз.

Даже если deprecated-опции ещё временно работают через "ignoreDeprecations": "6.0", в TypeScript 7.0 они будут полностью удалены.

Если вы уже видите предупреждения после обновления до 6.0, лучше устранить их заранее — до перехода на TypeScript 7.0 или экспериментов с нативными preview-сборками.

Ожидается, что TypeScript 7.0 выйдет вскоре после 6.0, чтобы обратная связь по миграции приходила быстрее.

Что дальше?

TypeScript 6.0 уже feature-complete. Теперь команда будет в основном заниматься только критическими исправлениями.

В ближайшие недели разработчиков призывают пробовать RC, заводить issues и делиться обратной связью.

Команда также публикует nightly-сборки:

  • в npm
  • в Visual Studio Code через ms-vscode.vscode-typescript-next

Параллельно продолжается работа над TypeScript 7.0. Для него тоже доступны nightly native preview и отдельное расширение VS Code.

Так что ставьте TypeScript 6.0 RC, пробуйте его в своём проекте и делитесь впечатлениями.

Happy Hacking!

— Daniel Rosenwasser и команда TypeScript


Теги: typescript, javascript, nodejs, compiler

Источник