Strona główna / Artykuły / Co tak naprawdę robi, a czego nie robi wsparcie Node.js dla TypeScripta w wersji natywnej

Co tak naprawdę robi, a czego nie robi wsparcie Node.js dla TypeScripta w wersji natywnej

Ten artykuł wyjaśnia, w jaki sposób Node.js uruchamia pliki .ts w sposób natywny poprzez usuwanie informacji typów, dlaczego pomija sprawdzanie typów oraz kiedy nadal potrzebny jest rzeczywisty krok budowania.

1595 słów

Znasz już ten proces. Tworzysz nowy projekt TypeScript, piszesz swój pierwszy plik .ts, próbujesz go uruchomić i od razu przypominasz sobie, że musisz najpierw wykonać cały rytuał konfiguracji: pobrać ts-node lub tsx, skonfigurować plik tsconfig.json, być może ustawić skrypt budowania oraz określić, czy celujesz w CommonJS, czy ESM. Żadna z tych czynności sama w sobie nie jest szczególnie trudna. To po prostu różne przeszkody, które gromadzą się, zanim napiszesz jakąkolwiek rzeczywistą logikę aplikacji, i zdarza się to za każdym razem, gdy rozpoczynasz coś nowego.

W tym roku w znacznej części rzeczywistych projektów Node.js cały ten proces po prostu zniknął. Wystarczy wpisać node file.ts – to działa bez żadnych problemów. Żadnych flag, żadnych dodatkowych zależności, żadnych plików konfiguracyjnych. Ta zmiana wdrożyła się po cichu, bez żadnego dużego ogłoszenia, ale jest to przykład usunięcia drobnych przeszkód, z którymi inaczej musielibyśmy się mierzyć wielokrotnie w ciągu tygodnia – a to razem tworzy coś, co naprawdę warto zgłębić.

Co tak naprawdę się dzieje

Mechanizm leżący u podstaw nazywa się type stripping, a nazwa ta jest niezwykle dosłowna: Node.js analizuje Twój kod TypeScript, usuwa adnotacje typowe i uruchamia to, co pozostaje w postaci zwykłego JavaScriptu. To właśnie cała idea.

// 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`;
}

Deklaracja interface całkowicie znika. Adnotacje takie jak : User i : string są usuwane. Pozostaje zwykły, poprawny JavaScript, który V8 wykonywa tak jak zawsze – nie ma żadnego specjalnego środowiska wykonawczego, żadnych polyfilli, nic nowego nie dzieje się koncepcyjnie w momencie wykonywania.

Za kulisami ten proces odbywa się za pomocą biblioteki o nazwie Amaro, która stanowi lekki „opakowacz” wokół @swc/wasm-typescript – kompilacji WebAssembly parsera TypeScript stworzonego przez SWC w języku Rust. Szybkość ta nie wynika z jakiejś sprytniejszej optymalizacji, lecz z faktu, że wykonywane jest znacznie mniej operacji niż w przypadku pełnego kompilatora. Biblioteka ta nie rozwiązuje problemów z typami w różnych plikach, nie sprawdza poprawności anotacji ani nie tworzy plików deklaracyjnych. Po prostu analizuje drzewo składni, usuwa elementy specyficzne tylko dla TypeScript i zwraca kod w formacie JavaScript. Właśnie ten wąski zakres działania sprawia, że jest tak szybka.

Wsparcie Node dla tej funkcjonalności przechodziło przez kilka etapów, zanim osiągnęła obecną formę: eksperymentalne wsparcie prostego usuwania typów pojawiło się w wersji v22.6.0, oddzielna flaga do obsługi bardziej złożonych struktur, takich jak enumy, pojawiła się w v22.7.0, a cała funkcjonalność stała się domyślnie stabilna zarówno w v22.18.0, jak i v24.3.0 – co oznacza, że dla kodu zgodnego z obsługiwaną składnią w ogóle nie są potrzebne żadne flagi. Warto zauważyć, że Node później całkowicie usunął tę flagę specyficzną dla enumów, decydując się na świadomie wąski i przewidywalny zakres zamiast próby obsługi całego języka.

Prawdziwe ograniczenia

Oto kluczowy punkt, który należy zrozumieć, jeśli zamierzasz polegać na tej funkcjonalności – i zasługuje on na jasne sformułowanie: usuwanie typów to nie to samo co sprawdzanie typów.

Usunięcie adnotacji typu nie potwierdza najpierw, że jest poprawna — po prostu ją usuwa. Dlatego plik zawierający rzeczywisty błąd typu, na przykład przekazanie ciągu znaków w miejscu, gdzie oczekiwano liczby, będzie działał bez żadnych problemów przy usuwaniu adnotacji typu, ponieważ w momencie faktycznej eksploatacji kodu informacje typowe, które mogłyby zasygnalizować problem, już nie istnieją. Wszystkie poważne źródła na ten temat zgadzają się co do tej samej rady: kontynuujcie używanie polecenia tsc --noEmit jako odrębnego kroku w waszym pipeline CI. Usuwanie adnotacji typu zastępuje krok budowania, a nie zadanie kompilatora polegające na faktycznym wykrywaniu błędów.

Bardziej istotną ograniczeniem jest to, która dokładnie składnia TypeScript nadaje się do usunięcia. Node obsługuje jedynie tak zwaną składnię nadającą się do usunięcia – konstrukcje językowe, które można całkowicie usunąć bez wpływu na to, jak kod działa podczas wykonywania. Znaczna część TypeScript nie spełnia tego kryterium, ponieważ generuje rzeczywiste zachowanie w czasie wykonywania, którego nie można po prostu usunąć:

// ❌ 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;

Każda z tych konstrukcji powoduje rażący błąd zamiast cichego, błędnego skompilowania — mechanizm usuwania elementów w Node jest celowo zaprojektowany tak, by zatrzymać proces i zgłosić błąd, zamiast domyślać się intencji użytkownika. Dekoratory w stylu starszych wersji, aktywowane za pomocą starej flagi experimentalDecorators, napotykają te same problemy z tego samego powodu. Nowsze dekoratory zgodne ze standardami TC39 to inna sprawa: są zdefiniowane w taki sposób, że przekształcają się w zwykłą składnię JavaScripta, więc nie ma nic specjalnego do usunięcia, a działają one w Node bez żadnych problemów.

Jak TypeScript się dostosował

Zamiast pozwalać programistom przypadkowo natrafiać na te ograniczenia, przeglądając jeden plik po drugim podczas uruchamiania kodu, zespół TypeScript szybko postanowił uczynić te zasady jawnymi. W wersji TypeScript 5.8 dodano nowy flagę kompilatora --erasableSyntaxOnly, która sprawia, że sam tsc odrzuca wszystkie opisane powyżej wzory, które nie mogą zostać usunięte, podczas kompilacji. Dzięki temu pytanie „czy Node rzeczywiście uruchomi ten kod?” przestaje być czymś, co odkrywa się w trudny sposób w czasie wykonywania, a staje się zasadą, którą można wprowadzić od razu jako wyraźną ograniczenie dla całego kodu.

// tsconfig.json
{
  "compilerOptions": {
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true  // pairs well with this —
                                   // keeps type-only imports explicit
  }
}

Warto włączyć tę opcję, nawet jeśli na razie nie planujesz usuwania tego kroku w procesie budowania, ponieważ daje ona jednoznaczną, zautomatyzowaną odpowiedź na to, czy twój kod spełnia wymagania — zamiast dowiadywać się o tym stopniowo, gdy coś przestaje działać.

Ilość pracy związanej z migracją, jaką to ujawnia, znacznie się różni w zależności od punktu wyjścia. Nowo stworzona usługa backendowa lub narzędzie wiersza poleceń zazwyczaj może od razu włączyć erasableSyntaxOnly bez większych zmian lub w ogóle bez żadnych poprawek. Baza kodu, która w dużej mierze polega na deklaracjach enum, lub taka zbudowana na frameworku zakładającym starsze dekoratory – starsze konfiguracje NestJS lub TypeORM to przykłady najczęściej występujące – wymaga rzeczywistej pracy nad przepisaniem kodu albo świadomego wyboru pozostania przy tradycyjnym procesie budowania zamiast migrować wszystko za jednym razem. Najbardziej niezawodny sposób na ocenę zakresu pracy przed podjęciem jakichkolwiek decyzji to włączenie erasableSyntaxOnly, uruchomienie raz komendy tsc --noEmit i sprawdzenie, ile błędów się pojawi. Ten jeden uruchomienie pokazuje rzeczywisty zakres problemu, zanim jeszcze zmienisz jakiekolwiek konfiguracje w czasie wykonywania aplikacji.

Praktyczne wskazówki

W zespołach, które już przeszły przez tę transformację, ukształtowała się dość spójna struktura podejmowania decyzji.

Pomijaj krok budowy dla usług backendowych, narzędzi z linii poleceń, wewnętrznych utilitów oraz samodzielnych skryptów – wszystkiego, co działa bezpośrednio pod Node’em i nie jest publikowane jako pakiet dostępny dla innych. To dokładnie ten typ przypadków, do których została stworzona funkcja usuwania elementów, a usługi zbudowane na Express lub Fastify powszechnie działają z nią od razu, bez konieczności zmian w kodzie.

Zachowaj krok budowania dla wszystkiego, co działa w przeglądarce, ponieważ przeglądarki w ogóle nie mogą wykonywać plików .ts — bez względu na to, jakie funkcje obsługuje Node na serwerze, nadal będziesz potrzebował narzędzia do pakowania. Zachowaj również krok budowania dla każdego pakietu npm, który publikujesz, ponieważ osoby go instalujące potrzebują skompilowanego JavaScriptu oraz plików deklaracji .d.ts, a nie ma gwarancji, że ich wersja Node w ogóle obsługuje usuwanie informacji typów. Dodatkowo zachowaj krok budowania dla każdego kodu, który nadal polega na starszych dekoratorach lub intensywnym użyciu enum, które jeszcze nie zostały przekonwertowane.

Niezależnie od wybranej ścieżki, kontynuuj uruchamianie polecenia tsc --noEmit w środowisku CI. Usunięcie kroku budowania usuwa jedynie etap kompilacji — nigdy nie miał on na celu eliminacji sprawdzania typów, a traktowanie go jako zamiennika dla tsc to jedyny prawdziwy sposób, w jaki ta zmiana może potajemnie kosztować cię bezpieczeństwo.

Rzeczywisty wniosek

To, co czyni tę zmianę interesującą, to nie tyle szybszość działania, chociaż szybszy cykl sprzężeń zwrotnych stanowi rzeczywistą, natychmiastową korzyść. Jest to sygnał dotyczący kierunku rozwoju TypeScript pod względem koncepcyjnym. Przez większość swojej historii TypeScript był opisywany jako język, który kompiluje się do JavaScript – odrębnego języka, który musi zostać przetłumaczony przed uruchomieniem. Usuwanie typów w Node sugeruje, że TypeScript zmierza ku temu, by być traktowany bardziej jak wariant JavaScripta, który środowisko wykonawcze może po prostu odczytać takim, jaki jest, przynajmniej jeśli chodzi o szeroki, codziennie używany podzbiór języka przez większość programistów. To nie jest cały język, i nigdy nim nie będzie – enumy oraz starsze dekoratory wciąż mają praktyczne zastosowania i nie znikają. Ale dla całego kodu, który ich nie wymaga, krok, który wcześniej znajdował się pomiędzy pisaniem kodu w TypeScript a jego uruchomieniem, przestał być obowiązkowy.

To znacznie większa zmiana, niż mogłoby sugerować skromne zainteresowanie, jakie wzbudziła.

Literatura pokrewna