Що насправді робить та чого не робить нативна підтримка TypeScript у Node.js
У цій статті пояснюється, як Node.js виконує файли .ts нативно шляхом видалення типів, чому він пропускає перевірку типів та коли все одно потрібен справжній крок збірки.
Ви вже знаєте, як це працює. Ви створюєте новий проект TypeScript, пишете свій перший файл .ts, намагаєтесь його виконати, і одразу ж згадуєте, що спочатку потрібно виконати цілий ритуал налаштувань: завантажити ts-node або tsx, налаштувати файл tsconfig.json, можливо, налаштувати скрипт збірки та вирішити, чи призначений проект для CommonJS чи ESM. Жоден з цих кроків сам по собі не є особливо складним. Це просто зайві труднощі, які накопичуються ще до того, як ви напишете будь-яку справжню логіку програми, і це відбувається щоразу, коли ви починаєте щось нове.
Цього року у значній кількості реальних проектів на Node.js весь цей процес просто зник. Достатньо лише ввести node file.ts — і все запрацює. Жодних флагів, жодних додаткових залежностей, жодних файлів конфігурації. Ця зміна впровадилась тихо, без жодних гучних оголошень, але це саме той вид усунення дрібних перешкод, з якими інакше доводиться стикатися щодня — і все це разом робить ситуацію справді вартою уваги.
Що насправді відбувається
Основний механізм називається видалення типів, і назва ця досить буквальна: Node.js аналізує ваш вихідний код на TypeScript, видаляє анотації типів та виконує решту коду у звичайному JavaScript. Ось і вся суть.
// Before: what you write
interface User {
name: string;
age: number;
}
function describeUser(user: User): string {
return `${user.name} is ${user.age} years old`;
}
// After: what Node.js actually executes, post-stripping
// (whitespace preserved, so line numbers stay accurate for debugging)
function describeUser(user) {
return `${user.name} is ${user.age} years old`;
}
Оголошення interface повністю зникає. Анотації на кшталт : User та : string також видаляються. Те, що залишається, — це звичайний, коректний JavaScript, який V8 виконує так само, як і завжди; не потрібен жодний спеціальний механізм виконання, жодні поліфіли, і під час виконання концептуально нічого нового не відбувається.
У закритому режимі цей процес виконується за допомогою бібліотеки під назвою Amaro, яка є легким обгортком навколо @swc/wasm-typescript — компіляції WebAssembly парсера TypeScript, створеної SWC на мові Rust. Швидкість тут не походить від якихось складних оптимізацій; вона зумовлена тим, що виконується значно менше роботи, ніж у повного компілятора. Він не обробляє типи у кількох файлах, не перевіряє правильність ваших анотацій та не створює файлів декларацій. Він просто аналізує дерево синтаксису, видаляє елементи, характерні лише для TypeScript, та повертає JavaScript. Саме такий вузький діапазон функцій є причиною його швидкості.
Підтримка цієї функції в Node пройшла кілька етапів, перш ніж досягти своєї поточної форми: експериментальна підтримка простого видалення типів з’явилась у версії v22.6.0, окремий флаг для обробки складніших конструкцій, таких як enum, з’явився у версії v22.7.0, а вся функція стала стандартно стабільною як у версії v22.18.0, так і в v24.3.0 — це означає, що для коду, який відповідає підтримуваній синтаксиці, зовсім не потрібні жодні флаги. Варто зазначити, що пізніше Node повністю видалила цей флаг, призначений спеціально для enum, обравши шлях обмеженого та передбачуваного функціоналу замість спроб підтримати всю мову.
Чесні обмеження
Ось те ключове, що потрібно зрозуміти, якщо ви плануєте покладатися на цю функцію, і його варто чітко сформулювати: видалення типів — це не те саме, що перевірка типів.
Видалення анотації типу спочатку не підтверджує її правильність — воно просто її видаляє. Тож файл, який містить справжню помилку типу, наприклад, передачу рядка там, де очікувався чисел, буде працювати без жодних проблем під час видалення анотацій типів, оскільки до моменту фактичної виконуваності коду інформація про тип, яка могла б виявити проблему, вже зникла. Усі авторитетні джерела на цю тему дають однакову пораду: продовжуйте використовувати tsc --noEmit як окремий крок у вашому CI-пайплайні. Видалення анотацій типів замінює крок компіляції, але не функцію компілятора щодо виявлення помилок.
Більш суттєва обмеження стосується того, яка саме синтаксис TypeScript підходить для видалення. Node підтримує лише так звану синтаксис, який можна видалити — конструкції мови, які можна повністю прибрати без зміни того, що робить код під час виконання. Значна частина TypeScript не відповідає цим критеріям, оскільки вона створює справжню поведінку під час виконання, яку неможливо просто видалити:
// ❌ Fails under type stripping — enums generate a real runtime object
enum Direction {
Up,
Down,
Left,
Right,
}
// ❌ Fails - parameter properties generate constructor assignment code
class Point {
constructor(public x: number, public y: number) {}
}
// ❌ Fails - this is a CommonJS-style module alias, not an erasable type
import fs = require('fs');
// ❌ Fails - angle-bracket type assertions look like real syntax to strip,
// but the parser can't tell it apart from JSX safely
const num = <number>someValue;
Кожна з цих конструкцій спричиняє фатальну помилку замість тихої неправильної компіляції — механізм видалення елементів у Node спеціально розроблений так, щоб зупинятися та повідомляти про проблему, замість того щоб вгадувати, що ви мали на увазі. Декоратори у стилі легасі, які активуються за допомогою старого флага experimentalDecorators, стикаються з тією самою проблемою з тих самих причин. Новіші декоратори, що відповідають стандартам TC39, — це інша справа: вони описані таким чином, що компілюються у звичайну синтаксис JavaScript, тож немає нічого особливого, що можна було б видалити, і вони працюють у Node без жодних проблем.
Як адаптувався TypeScript
Замість того, щоб дозволяти розробникам випадково натрапляти на ці обмеження по одному файлу під час виконання коду, команда TypeScript швидко вирішила уточнити правила. У версії TypeScript 5.8 було додано новий флаг компілятора --erasableSyntaxOnly, який змушує сам tsc відхиляти будь-які з описаних вище непридатних для обробки синтаксичних структур під час компіляції. Це перетворює питання „Чи справді Node виконає цей код?“ з чогось, що потрібно з’ясовувати способом проб і помилок під час роботи програми, на правило, яке можна застосувати заздалегідь як чітке обмеження для всього кодового базису.
// tsconfig.json
{
"compilerOptions": {
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true // pairs well with this —
// keeps type-only imports explicit
}
}
Варто увімкнути цей параметр навіть якщо у вас поки що немає планів щодо видалення кроку збірки, адже це дає вам однозначну, автоматизовану відповідь на те, чи підходить ваш код — замість того, щоб дізнаватися про це поступово під час виникнення проблем.
Обсяг роботи з міграції, який це виявляє, сильно варіюється залежно від початкової ситуації. Новостворений бекенд-сервіс чи інструмент командного рядка зазвичай може одразу увімкнути erasableSyntaxOnly без майже жодних змін. Однак кодова база, яка сильно залежить від оголошень enum, або та, що побудована на фреймворку, який використовує старі декоратори — такі як старі версії NestJS чи TypeORM, — вимагає справжніх зусиль щодо переписування коду або свідомого вибору залишитися з традиційною схемою будування проекту замість масової міграції всього одразу. Найнадійніший спосіб оцінити обсяг роботи перед тим, як щось починати, — це увімкнути erasableSyntaxOnly, виконати команду tsc --noEmit один раз та подивитися, скільки помилок з’явиться. Цей один запуск покаже вам реальний масштаб проблеми ще до того, як ви будете змінювати будь-які конфігурації під час виконання.
Практичні рекомендації
У командах, які вже пройшли цю трансформацію, сформувалася досить послідовна система прийняття рішень.
Відмініть крок збірки для бекенд-сервісів, інструментів командного рядка, внутрішніх утиліт та автономних скриптів — будь-чого, що запускається безпосередньо під Node та не опубліковується як пакет для використання іншими. Саме для таких випадків була створена функція видалення елементів, і сервіси, побудовані на Express чи Fastify, зазвичай починають працювати з нею одразу, без необхідності змін у коді.
Зберігайте крок збірки для всього, що працює в браузері, адже браузери зовсім не можуть виконувати файли .ts — вам все одно знадобиться інструмент для об’єднання файлів, незалежно від того, що підтримує Node на сервері. Також зберігайте крок збірки для будь-якого пакету npm, який ви публікуєте, оскільки люди, які його встановлюють, потребують скомпільованої JavaScript-кодування та файлів оголошень .d.ts, і ви не можете бути впевнені, що версія Node у них підтримує навіть видалення типів. Крім того, зберігайте крок збірки для будь-якого кодового базису, яка все ще використовує застарілі декоратори чи інтенсивне використання enum, яке ще не було перетворене.
Яким би шляхом ви не обрали, продовжуйте виконувати команду tsc --noEmit у середовищі CI. Видалення кроку збірки лише усуває етап компіляції — він ніколи не призначався для видалення перевірки типів, і розглядати його як заміну tsc — це єдиний спосіб, через який ця зміна може тихо позбавити вас безпеки.
Основний висновок
Те, що робить цю зміну цікавою, — це не стільки прискорення, хоча швидший цикл зворотного зв’язку є справжньою та негайною перевагою. Це сигнал про те, у якому напрямку концептуально розвивається TypeScript. Протягом більшої частини своєї історії TypeScript описувався як мова, яка компілюється у JavaScript — окрема мова, яку потрібно перекладати перед виконанням. Процес видалення типів у Node тихо свідчить про те, що TypeScript поступово стає більше схожим на варіант JavaScript, який середовище виконання може просто читати без змін, принаймні щодо того широкого, повсякденного підмножини мови, яку насправді використовують більшість розробників. Це не вся мова, і цього ніколи не буде — енумерації та старі декоратори все ще мають реальні сценарії використання та не зникають. Але для всього коду, якому вони не потрібні, крок, який раніше існував між написанням коду на TypeScript та його виконанням, більше не є обов’язковим.
Це значно більша зміна, ніж можна було б припустити з огляду на скромну увагу, яку вона отримала.Пов’язані статті
- Заміна Jest на вбудований тест-запускач Node у Node 24 — Приклад реального міграційного процесу демонструє, як вбудований тест-запускач Node 24 та підтримка TypeScript допомагають скоротити час виконання тестів, водночас усуваючи чотири залежності.
- Роль мосту TypeScript 6 на шляху до нативного компілятора TS 7 — Дізнайтеся, як TypeScript 6 оновлює стандартні налаштування, механізми розрішення модулів та синтаксис імпорту, щоб підготувати кодові бази до швидшого компілятора TypeScript 7, заснованого на Go.