🔗

Node.js で TypeScript を直接実行してみた記録とノウハウ

に公開

最近の Node.js は TypeScript のコードを直接実行できます。v23.6.0 以降(最新の v24 系含む)ではそのまま実行できますし、最新の LTS である v22.15.0 では実行時に --experimental-strip-types のオプションを指定することで実行できます。

どんなものなのか実際に試してみました。

まとめ・注意点

  • 学習やお試しのレベルではかなり有用。
  • プロダクションレベルでは特にまだメリットがない。
  • エディタ (VSCode) の型エラーと実行時エラーは別。
    • VSCode に型エラーがあるが、実行できることがある。
    • VSCode に型エラーがないが、実行時エラーが出ることがある。
  • TypeScript 独自の機能を使うにはオプションを指定する必要がある。
  • ファイルを import するときは拡張子が必要。
  • 型を import するときは type と書く必要がある。

実行してみた環境

macOS Sequoia 15.4.1
Node.js v24.0.1
VSCode v1.100.0

(記載のコードを v22 系で実行する場合は、実行コマンドに --experimental-strip-types のオプションを指定してください)

前提

Node.js における TypeScript の実行は「Type Stripping」という方法を用いています。Type は「型」、Stripping は「除去」という意味ですね。これは TypeScript の型に関する構文をスペースに置き換えて(除去して) JavaScript にして、型チェックを行わずに実行するというものです。TypeScript が JavaScript のスーパーセットであることを活用した方法です。

しかしながら完全な TypeScript の実行ではないので制限がありますし、Node.js と tsc ではちょっとした違いがあります。

TypeScript を実行してみる

まずは簡単な TypeScript のコードを動かしてみます。

適当なフォルダに main.ts ファイルを作り、以下のように記載します。渡された 2 つの数値を足し合わせる add 関数と、その結果をコンソール出力するコードです。

main.ts
function add(a: number, b: number) {
  return a + b;
}

console.log(add(1, 2));

ターミナルを開き、node main.ts コマンドでこのコードを実行すると、結果が出力されます。

3
(node:16848) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)

実行結果である 3 が出力され、TypeScript のコードが問題なく実行できています。

余分なメッセージが出力されていますが、これは実験的な機能を使っているという警告メッセージです。Node.js における Type Stripping を使った TypeScript の実行はまだ安定化されていないということを知らせているだけで、出力の見た目的に邪魔なだけであって特に支障はありません。

以降、この警告メッセージは省略します。

型にエラーがある状態での挙動

TypeScript では型チェックが行われますが、それがどのような挙動になるか試してみます。

先ほどのコードの add 関数の引数は 2 つとも number 型でしたが、呼び出す部分で片方を文字列 string 型に書き換えてみます。

main.ts
function add(a: number, b: number) {
  return a + b;
}

console.log(add("1", 2)); // ← 引数を文字列に変更すると、エディタではエラーが出る

すると VSCode では 型 'string' の引数を型 'number' のパラメーターに割り当てることはできません。ts(2345) のエラーが表示されます。引数の型が合っていないことによるエラーですね。

では、このコードを node main.ts で実行してみましょう。その出力は以下のようになります。

12

実行はできていますが、変な結果になっています。

勘の良い方は分かると思いますが、これはコードが JavaScript として実行された結果です。JavaScript では 文字列 + 数値 は「数値を文字列に変換して、文字列同士として結合する」という挙動となるため、"1" + "2" となって連結された文字列 "12" が出力されているわけです。

エラーが出ずに実行されてこのような結果になるのは、先述の通り Node.js が TypeScript のコードを Type Stripping して実行しているからです。つまり前処理として、元のコードを「TypeScript の型に関する構文をスペースに置き換える」ということで、まずは以下のような JavaScript のコードに変換します。

function add(a       , b       ) { // ← 型の記述がなくなる
  return a + b;
}

console.log(add("1", 2));

これは文句なしに JavaScript のコードです(余分な余白は関係ないため)。これが JavaScript として実行されるわけですが、JavaScript は動的型付け言語であるため、add の引数である ab にどのような値も渡せますし、それを + で足し合わせることが可能です。その結果、上記のコードの出力は 12 になるわけです。

今回は文字列を渡すようにしましたが、オブジェクトや配列や undefined などを関数に渡すことによって、もっと変な結果にすることもできます。型がない言語は大変です。

ということで、VSCode での型エラーを無視して実行することも可能ではありますが、大抵の場合は不正な出力やクラッシュといったひどい結果を引き起こすので、やめておいた方が無難です。

JavaScript にはない TypeScript 独自機能の挙動

JavaScript にはない TypeScript 独自の機能として、enumnamespace などがあります。こうした構文は、Type Stripping で型の構文を削除するだけでは JavaScript になりません。通常の tsc による TypeScript のコンパイル時には、これらの使用箇所を別の JavaScript のコードへ書き換えることで機能を実現しています。これがどうなるのか試してみます。

例として enum を使ったコードを書いてみます。

main.ts
enum Value {
  One = 1,
}

console.log(Value.One);

このコードは VSCode 上ではエラーが出ません。TypeScript としては正しいコードであるためです(後述)。

しかし実行するとエラーになります。以下のようなエラー(抜粋)が出力され、これは「enum は型情報を削除する Type Stripping だけではムリ」と言っています。

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

オプション指定による実行

これには回避策があり、実行時に enum などに対応するオプションを追加指定することで実行できます。--experimental-transform-types がそれで、名前の通り TypeScript 独自の enum などを上手いこと変換してから実行してくれます。

したがって、オプションを指定した node --experimental-transform-types main.ts のコマンドを実行すると、結果である 1 が出力されます。

ただこうした、削除するだけで JavaScript にならない構文を避ける風潮がコミュニティにはあるため、使わないようにしていく方が良さそうです。

VSCode でエラーが出ない理由

さて、ここで気になるのが、編集時に VSCode ではエラーが出なかったのに実行時にはエラーが出た、という部分です。この実行時のエラーは動的型付け言語のようで嬉しくありません。

こうなる理由は、VSCode と Node.js で、TypeScript への対応状態が異なるためです。

- TypeScript 型の構文 enum などの独自機能
VSCode 完全対応 対応 対応
Node.js 一部対応 対応 (デフォルトでは)非対応

したがってコードに enum が書かれていたとき、VSCode での編集時には正しい TypeScript のコードであるためエラーが出ず、Node.js での実行時には対応していない構文であるためにエラーになったわけです。

別の TypeScript ファイルの読み込み

関数を別ファイルに定義して読み込んでみます。新しい lib.ts ファイルを作り、そちらへ add 関数を移動させます。

lib.ts
export function add(a: number, b: number) { // ← export する
  return a + b;
}
main.ts
import { add } from "./lib.ts"; // ← 拡張子を書く必要がある

console.log(add(1, 2));

これを import して使用しても、型チェックは行われますし、問題なく実行されます。

1つ注意が必要なのは、import するファイル(モジュール)の拡張子を書く必要があることです。拡張子がないと、実行時に「Cannot find module 〜〜」のエラーが出ます。

Node.js の import では拡張子が必須ですが、通常の tsc を使った開発時には拡張子は書かないことが多い(これには複雑な理由がある)ので、この違いは忘れがちです。心に留めておきましょう。

なお、拡張子がないときに VSCode での編集中にエラーにならないのは、上記と同じように、TypeScript (tsc) 的には拡張子がないことが許容されているためです。

別のファイルで定義した型の読み込み

別のファイルで定義した型を読み込んでみます。

lib.ts
export type T = {
  value: number;
};
main.ts
import { T } from "./lib.ts";

これを実行すると、以下のようなエラーが出ます。

SyntaxError: The requested module './lib.ts' does not provide an export named 'T'

./lib.ts では Texport していない」と言っていますが、そんなことはないですね。

実はこれ、Node.js で型を import する場合、型に type と記載する必要があるためです。つまり、以下のいずれかのように書く必要があります。

import { type T } from "./lib.ts";
import type { T } from "./lib.ts";

main.ts をいずれかに書き換えるとエラーなく実行できるようになります(何も起こりませんが)。

このように import type の記載がないときに実行時にエラーになる理屈は以下の通りのようです。

  1. lib.tsT の型定義が Type Stripping で削除される。
  2. main.tsimport で Type Stripping が動いたとき、それぞれどのような種類のものを import しているのか分からないのでそのまま残る。
  3. lib.ts に存在しない Timport することになり、実行できずにエラーとなる。

実行した結果、 3. の状態がエラーメッセージとして表示されています。

したがって、「main.tsimport している T は型定義である」という情報を import type と書くことで与えれば Type Stripping で削除できる(import 自体を消せる)ため、エラーにならなくなるようです。

この import type は tsc を使うときは基本的に不要なので忘れがちです。このパターンは特にエラーメッセージが役に立たないという罠なので、覚えておきましょう。

外部ライブラリの使用

外部ライブラリで型が効くか試してみます。コンソール出力に色を付ける軽量ライブラリの picocolors を使用してみます。

コンソールで npm i picocolors を実行して追加することで、package.jsonpackage-lock.json が作成されます。実行時に警告が出るので、package.json"type": "module" を追記して警告を抑制しておくのもよいでしょう。

main.ts
import pc from "picocolors";

console.log(pc.red(42));

これは問題なく実行され、赤い 42 が表示されます(環境によっては赤文字にならないこともあります)。

特に型に関する追加は行っていませんが、VSCode での編集中には型が有効になります。pc. を打ったときに関数名の候補が出てきますし、pc.red([]) のように許容されていない型の値を渡すと型エラーになります。

これは node_modules/picocolors/ に型定義ファイルである types.d.tspicocolors.d.ts が存在し、これを VSCode が参照しているからです。試しにこれらのファイルを削除してみると、型が無効になることがわかります。

ということは当然ではありますが、型定義がないライブラリなどは型が使えないということです。例えば node:fs などを使う場合、そのままでは型情報がないため、エディタ上で型が効きません。それどころか TypeScript に関する挙動により「node:fs の型宣言がない」というエラーが出ます(実行はできますが)。これを解消するためには @types/node を追加するなどの対応が必要です。

最後のまとめ

Node.js での TypeScript の実行は、学習やお試しといった手軽な方法として有用です。Node.js をインストールするだけで実行できるので、tsconfig.json の設定などの環境構築で詰まるケースを減らせておすすめです。

ただ実行前に型チェックを行うような仕組みではないので、プロダクションレベルで使うのは躊躇します。CI で事前に型チェックは行えますが、それなら JavaScript にトランスパイルしてしまえばよいだけなので、現状では特にメリットがありません。

ただ今後、周辺環境も合わせてまだまだ進歩していくと思われるので楽しみなところです。

Discussion