- Node.js PR #53725 がマージされ、
--experimental-strip-types フラグで TypeScript ファイルの実行を実験的にサポートするようになった
- この機能は TypeScript ソースから 型構文を除去して JavaScript ソースへ変換し、変換過程で型チェックは行わない
- 初期実装では
Enum、namespaces のような TypeScript 専用機能の変換は行わないため、型の除去だけで JavaScript になるコードに限定される
- 実装には
@swc/wasm-typescript が選ばれた。理由は Rust や Go を Node.js ツールチェーンに追加しなくてよいという 単純さ
- 議論では、ESM の必須ファイル拡張子ルール、
.js import が実際には .ts ファイルを指す TypeScript の慣習、ソースマップとエラースタック位置のコストが主な制約として扱われた
--experimental-strip-types の動作方式
--experimental-strip-types を設定すると、Node.js が TypeScript ファイルを実行できる
- Node.js は TypeScript ソースコードを JavaScript ソースコードへ トランスパイルする
- トランスパイル過程では 型チェックは行わず、型情報は破棄される
- 例は次のとおり
const foo: string = "foo";
const foo = "foo";
型の除去だけを選んだ理由と初期の制限
- この PR の核心は、TypeScript 構文のうち 型構文の除去である
- 他のランタイムでは、
enum のように JavaScript にはない TypeScript 専用機能を JavaScript に変換することもある
- この PR の初期実装では、そのような変換は行わない
Enum は使用不可
namespaces は使用不可
- その他の TypeScript 専用機能も変換対象ではない
- 実装の詳細と制限については、PR の変更内容に含まれる
typescript.md を参照するよう案内されている
実装ツールとして @swc/wasm-typescript を選択
@swc/wasm-typescript は 小さなパッケージで、wasm ファイルとそれをバインドする JavaScript ファイルで構成されている
- 他のツールには、Rust や Go を Node.js ツールチェーンに追加する必要があるものがあった
- SWC は Deno でも同じ目的で使われており、PR 作成者はこれを 実証済みの選択肢と見ている
- 将来的には、この機能が ネイティブレイヤーで実装される可能性があると見ている
- この PR は nodejs/loaders#217 を参照している
import 解決と拡張子の議論
import foo from "./foo" のような 拡張子なしの import はサポートされない
- Node.js ESM には mandatory file extensions ルールがあり、拡張子なし import の非サポートは意図された動作として扱われている
- TypeScript のモジュールコードベースでは、実際のファイルが
.ts でも import 文には .js 拡張子を書くことが一般的だという意見が出た
- 例:
import foo from './foo.js'
- 実際のファイルシステムには
./foo.js がなく、./foo.ts だけがある場合
- PR 作成者は、この問題は今後の反復作業で扱える可能性があり、拡張子の推測には追加のファイルシステム確認とヒューリスティックが必要だと見ている
./foo.js がなければ ./foo.ts を確認
./foo.ts がなければ ./foo.js を確認
- この議論は nodejs/loaders#208 で追跡されている
ソース位置、ソースマップ、エラースタック
- レビューでは、エラースタックの 正確なソース位置が短期的に必要だという意見が出た
- 可能な方法の一つは、型の除去過程でソース位置を変えず、ソースマップ生成とマッピングを避けること
- ソースマップにはメモリ使用量と性能面でコストがあり、単純ではないという制約が言及された
- PR 作成者は、SWC がすでにソースマップをサポートしているため、これを有効化するフラグを追加できると見ている
- 除去された構文を空白で置き換え、元のエラースタックを変えないオプションを SWC に要望することもできると見ている
安定化前に残る設計上の論点
- TypeScript サポートは二つの軸に分かれる
- 型構文の除去
.ts ファイルを受け入れるよう解決ルールを調整
- 解決ルールの変更は既存の TypeScript ローダーと衝突する可能性があるという懸念が出た
- PR 作成者は、この PR には解決ルールのオーバーライドはなく、ファイル拡張子が必要だと回答した
allowImportingTsExtensions によって import './file.ts' 形式をサポートでき、この方式は Deno とも合うという意見が示された
- 同じソースファイルを直接実行し、ビルドツールで JavaScript 出力も作るには、
file.ts を file.js に変える責任はビルドツールが担えると見ている
マージと後続作業
- PR は 2024年7月24日に
nodejs:main へマージされた
- 変更規模は 8コミット、89ファイル、2,190行追加と25行削除
semver-minor、experimental、module、esm、notable-change などのラベルが付けられた
- Web サイトの TypeScript 案内ページでこの機能に言及するかについて議論があり、関連する更新 PR もリンクされた
- 配布メタデータに、Node バイナリが型の除去に使用する TypeScript バージョンを含める案も後続イシューとして議論された
1件のコメント
Hacker News のコメント
TypeScript の型削除は、TypeScript 構文なしには不可能です。型削除はトークン単位の作業ではなく、TypeScript 構文は変わり続けています。
たとえば
foo < bar & baz > ( x )は、TypeScript 1.5 ではbar&bazが有効な型式ではなかったため(foo (x))のようにパースされましたが、交差型演算子が追加された後はfoo<(bar & baz)>(x)としてパースされ、foo(x)へと曖昧性が解消されました。最新の TypeScript 機能を使い続けるには JS にコンパイルするか、Node のバージョンを常に最新に保つ必要があり、Node LTS にとどまることを好むチームにとっては受け入れがたい妥協になり得ます。
コアには安定版を置き、TypeScript が新機能や破壊的変更を出したり、ユーザーが新しい実験的機能を使いたい場合は NPM から別途アップグレードできるようにする方式です。
こうすれば、ユーザーを固定せずに Node.js リリースの 3 年間のライフサイクル中、特定の TypeScript バージョンをサポートできます: https://github.com/nodejs/loaders/issues/217
最後の大きな構文変更はおそらく 2.8 の 条件付き型で、型の位置に三項
if演算子に似た構文が入った時でしょう。型チェックまで行うなら型モデルは 2.x 以降かなり変わりましたが、構文は概ね安定しており、TypeScript の革新も大半は型モデルと型推論の側にありました。まだ初期段階の Stage 1 ですが、型チェックではなく型削除を目的とした TypeScript の型構文の相当部分を標準化しようとする試みもあります: https://github.com/tc39/proposal-type-annotations
最新の V8 や Node 機能にあまり関心がなく LTS にとどまるチームは、最新の TypeScript 機能にもそれほど関心がない可能性が高いでしょう。
Node.js が TypeScript ファイルを直接実行できるなら、TypeScript コンパイラは型削除や JavaScript 変換の代わりに型チェッカーとしてだけ使えるようになります。Python で型チェッカーが型を検査してそのまま残し、インタプリタは型ヒントを無視する方式に似ています。
興味深いことに Python では、同じ型ヒント構文を使いながらも意味論は異なる人気の型チェッカーが複数生まれました。一方 JavaScript では、TypeScript が事実上唯一の人気の型チェッカーになったようです。
Python では型をチェックせず、ソースに型だけを書いて、より便利なコメント構文のように使う場合もあると聞きました。Node.js が型の無視をサポートすれば、JavaScript でもそのようなやり方が可能になります。
個人的に JSDoc 使用で最大の不便はインポート構文でしたが、TypeScript 5.5 以降で変わったため、今では問題ではなくなりました。
ただし一部のバンドラはすでに SWC のような非公式コンパイラを使ってこの作業を行っており、Node もこの機能で SWC を試験中です。
型ヒントは単なるコメントだけでなく、その名のとおり IDE がよりよい補完を表示するためのヒントでもあります。
これは言語仕様の一部になるべきだと思います。
この機能がフラグの裏側ではなくデフォルトになったとき、NPM エコシステムがどう反応するのか気になる。コントリビューターが NPM モジュールを配布する際に、今後も CJS と ESM のバージョンをビルドするのか、それとも
package.jsonにengine: nodejs >= 25だけを付けてビルド段階を省くのかは分からない個人的には、TS のソースを持つ NPM モジュールが
dist/.cjsの配布をやめれば、デバッガーやconsole.logをどこに入れればよいかが明確になって、むしろ良いと思う。NPM コントリビューターの立場でも、ビルド段階をなくす誘惑は大きいはずだただし、依存関係の一つでも TS ファイルを配布すると、「全員が TS ファイルを受け取る」という前提が急速に広がり得る。Node.js がこれをオプトインの実験フラグの外に出した瞬間、コミュニティ全体が利用者に TS ファイルの受け入れを暗黙に期待するようになりそうだ
そうなると数か月以内に Firefox と Safari も受け入れるよう圧力を受け、すべての JS コンパイラが TS の型注釈を捨てざるを得なくなるだろう
個人的には、それでも構わない。TS を使う側でも、何らかの架空の JS ユーザーをサポートするために、TS コードを JS と
.d.tsに変換する段階を NPM モジュールに入れているからだ。とはいえ Node が.tsファイルを受け入れると、その変換段階に気づかないまま取り除けてしまうので、NPM 公開者がjs/d.tsファイルの配布をやめても壊れたことに気づかない事態を防ぐ仕組みが気になるビルド段階なしで TypeScript コードをアップロードでき、他の開発者がインストールすると、変換済みの JavaScript ではなく元の TypeScript ソースを見られるようにする方式だ
.tsファイル実行はサポートしない。エコシステムを壊さないための主要な制約の一つだ.cjsと.mjsに変換しているが、おそらく 3 種類すべてのバージョンを配布することになると思う新しくゼロから書くプロジェクトなら TypeScript だけを配布し、変換はしないかもしれない
追記: PR 作成者であり Node.js 技術運営委員会メンバーと思われる satanacchio の兄弟コメントによると、TS サポートはトップレベル項目向けで、
node_modules内のライブラリには適用されないとのこと.tsを npm に配布したいが、TypeScript チームはtsconfigの問題と性能の問題のため、これに非常に反対していた。それでも期待はしているいつか Node が JS でこれらの型をランタイムでリフレクションできるようにするかもしれない
そうなれば非常に大きな利点になる。Python では型をリフレクションできるため、pydantic のような優れたツールが型から検査を生成できる
単一の標準表記だけで、型検査、ランタイムデータ検証、API 生成、API ドキュメント生成を得られる
現在の JS では、Zod のようなツールが
const mySchema = z.string();のように、TypeScript がすでに行っていることを実質的に再発明しなければならないz.string()は TypeScript のような型安全性だけを提供するわけではない。TypeScript はコンパイル時の型検査を提供し、Zod はランタイム検証とパースを追加するz.string()はmySchemaをパースと検証が可能な関数型スキーマにする。mySchema.parse("some data")は成功し、mySchema.parse(321)は例外を投げるランタイム検証とプロセス内チェックが必要な場所で使ったことがあるが、かなりうまく動作したし、
const A = z.string(); type A = z.infer; // stringのように型も取り出せるつまり Zod で先に型を定義し、そこから型を推論すれば、コンパイル時とランタイムの型検査を両方得られる。ただし、動きの速い小さなコードベースには過剰かもしれないが、プロセス内検証が必要な状況ではかなり有用で、作業量も大きくない
ただし TypeScript はプラグインを許可している。あまり使われてはいないが、結果の JavaScript に変換される別の機能を言語に追加できる
ランタイム型に関連するプラグインとして Typia があり、
assert(myValue)のようなガードで TypeScript の型シグネチャをランタイムで使えるようにしてくれる。この関数呼び出しを横取りし、変換後の JavaScript に渡された変数の性質を検査する網羅的なif文を生成する今後 4〜6 年以内に言語の一部になるとは思わないが、今日でもこうしたことを可能にするライブラリはすでにある
実際には健全な型システムが必要だが、TS は意図的に健全ではない。TS のように悪いコーディング慣行を許容せず、プログラムを高速にできる設計を強制する型システムが必要だ。Hindley-Milner 型システムが必要だ
モジュールを型付きコードにしたいなら、
"use type"を追加する形でよい。このモードでは型強制変換のような悪い部分を禁止し、オブジェクトの形や値の型を変えたり、任意の値を混ぜた配列を作ったりするなど、性能を損なう要素を防ぐべきだ型なしモジュールから入ってくるデータは変換されるか、変換できなければエラーを投げる。そうすればコンパイラは、より強い型保証と途中で諦めるリスクがない状態で、型付きコードを深く最適化できる
ValueErrorのサブクラスか、オブジェクトがSomeClassのインスタンスかを判別できるという意味なのか気になる。TS 初心者なので、配列、オブジェクト、数値、文字列以外の型は JS にはなく、Zod や型ガード関数は「信じてくれ」程度の普通のオブジェクトを返すものだと思っていたBunの開発者体験はこの領域ではかなり前例のないレベルで、今ではほとんどのユースケースがカバーされているか、
bun runで実行スクリプトを書くときにBunがクラッシュしなくなっている一方でNodeは、importで拡張子を要求しないように設定することもできず、
tscがコンパイル結果に.js拡張子を自動で付けるようにもできないため、バンドラーを追加する必要があるネイティブTypeScriptサポートはこの些細な不便をかなり解消するだろうが、安定化してもユーザー体験や性能がBunに匹敵するとは想像しにくい
開発者体験とツールも最高水準。Denoは型チェックもでき、記憶ではTSCをバンドルしている
Bunは話題性が大きいが、真剣な作業には現時点ではDenoのほうが明らかに良い選択肢。それでもBunのビジョンと実行力は印象的で、開発者にとって良いことだ
bun installの実行に失敗し、一度もまともに動かなかった: https://github.com/search?q=repo%3Aoven-sh%2Fbun+bun+install...TypeScriptランタイムを長く望んできたし、TypeScriptも本当に好きだが、ずっと前にJavaを離れたのに、結局Javaにずっと近い何かを探すことになったのは面白い
結局、自分たちが欲しかったのはJIT、より豊かな型システム、漸進的型付けを備えたJavaだったようだ。npmエコシステムにも欠点は多いが、このエコシステムでライブラリを使うのははるかに負担が少なく、より楽しい
驚くことに、Rustは言語スペクトルの反対側にあるが、似た感覚を与えることもある
追記: JITという表現は適切ではなかった。雑にJITと書いたが、JVMとV8で何かを実行するときの起動時間とランタイムの違いを言いたかった。Javaは重く感じるが、JavaScriptエコシステムははるかに機敏に感じる
TypeScriptがJavaにより近いのかもまったく分からない。両者が共有しているのは「Javascript」という誤った名前と命令型プログラミングのサポート程度で、残りは非常に異なると思う
ただしJavaにあってTypeScriptで恋しいのはランタイム型リフレクション。TypeScriptエコシステムにはこれを回避する方法が無数にあるが、どれも少し見苦しいと感じる
AoTでもよく、単一バイナリの選択肢があるとよい。理想的にはランタイムがなく、賢い除去ができるとよい。ブラウザ上でJITでも動けばさらに良い
exhaustiveなパターンマッチングのような、コンパイラと型チェッカーの便利機能も欲しい
自分の好きなDenoの機能がNodeに直接入ってきている。とても良い
これで型の除去のために常にesbuildをインストールしなくてもよくなるかもしれない。TypeScriptでスクリプトを書いて使うのがずっと簡単になりそうで期待している
最近は一回限りのスクリプトにはPythonを好んでいたが、型の面では個人的にTypeScriptのほうがPythonより良いと思う。特に数カ月後に見返す大きなスクリプトは、型の恩恵を大きく受ける
ts-nodeもあるが、個人的にはtsxのほうが好きhttps://github.com/privatenumber/tsx
Nodeにとって本当に出来事の多い1カ月だった。まずv22.5.0でnode:sqliteが追加され、今度はTypeScriptサポートも入ってきている。Nodeの向かう方向が気に入っている
PRの作者です。何でも聞いてください
かなり前からバックエンド作業に Node.js を使い始めたが、PHP でコードを書くより多くの利点がありつつ、Java の問題をあまり持ち込まないように思えた
ただし Node はやや粗削りで、望む言語にするにはあれこれ付け足さなければならない言語のように感じた。結局 Go を使い始めたが、ずっと扱いやすく、時にはかなり冗長ではあるものの、型安全性のおかげでコーディングが単純になった
TypeScript は良い選択肢に見えたが、結局また別の継ぎ足しだった。TypeScript を Go の代わりに使うことで、どんな価値が得られるのかよく分からない。明確な型があるのは良いが、Go で解決されている言語上のほかの問題を解決してくれるわけではない。Deno も一部は解決している
Node が Go に対して持つ大きな利点は、素早くプロトタイプを作れることだが、TypeScript を使うとその利点がかなり小さくなるように思う。だからこれが良い前進なのか、それとも Node を良い選択肢にしていた特性を失わせるものなのか判断しにくい
「継ぎ足し」という表現を見ると、問題は言語そのものというより、Django のようにすべてを標準で管理してくれる意見の強いフレームワークがないことかもしれない。Django を使うのは好きだが、定められた道から外れるのは少し難しくなる
cdktfと一緒に使っており、Go より論理的な選択に見えた