TypeScript の実行環境を ts-node から Node.js の直接実行に移行した

目次

ソフトウェアエンジニアのhataです。ハイヤールーでは業務委託として携わらせていただいています。今回は TypeScript で書かれたスクリプトの実行環境を ts-node から Node.js での直接実行に移行した話をします。

3行で要約すると

  • Node.js v22.18.0(v24系では24.0.0)以降、特別なフラグなしで TypeScript で書かれたコードを node コマンドで直接実行できるようになった
  • 移行手順は tsconfig の erasableSyntaxOnly、 allowImportingTsExtensions オプションを有効化 -> node 直接実行の順番がベター
  • 移行する TypeScript のスクリプトは別 package で管理した方がいい

背景

HireRoo のフロントエンドはモノレポになっており、BFF(GraphQL のバックエンド/クライアント)、UI層のプレゼンテーションコンポーネント、コーディング試験用の Monaco Editorベースのコードエディタなど、それぞれが独立したパッケージとして開発されています。それぞれのパッケージでは、TypeScript で書かれたスクリプトを開発ツールチェインとして使用しています。GraphQL 用のコード生成スクリプトや、ビルドプロセスを補助するスクリプト、CI 環境で実行するバリデーションスクリプトなど用途は様々です。

packages/
├── app
├── app-definition
├── app-helper
├── app-monitoring
├── app-store
├── backend
├── backend-definition
├── backend-helper
...

これらのスクリプトの実行環境として、これまで ts-node を使用していました。しかし、Node.js が v22.18.0(v24系では24.0.0) 以降、特別なフラグなしで TypeScript で書かれたコードを node コマンドで直接実行できるようになった(Type Stripping 機能と呼ばれている)ことで、ts-nodetsx をはじめとしたサードパーティの TypeScript スクリプトの実行ツールは前提条件ではなくなりました。

また、TypeScript 5.8 では erasableSyntaxOnly オプションが導入され、JavaScript でサポートされていない TypeScript 固有の構文をチェックできるようになりました。

これらの機能を組み合わせることで、Node.js の実行環境において、ts-node の役割が失われつつあります。そこで、TypeScript で書かれた各種ツールチェインを Node.js での直接実行に移行することにしました。

実際の移行手順

1. Node.js バージョンアップ

Type Stripping 機能は、v22.18.0 、v24.0.0 で安定版となりました。プロジェクトで使用していた Node.js のバージョンは 22系でしたが、v24系が移行タイミングで Active LTS になったこともあり、Node.js を v24系にアップデートしています。 ただ、移行対象のライブラリの一つが firebase-tools を利用しており、これを v24系にアップデートすると更新に失敗する問題が発生しました。しばらく依存ライブラリ側の対応待ちとなっていましたが、issue を watch し、解決済みとなったタイミングでバージョンアップを実施しました。

Node.jsのメジャーバージョンは6か月間 Current ステータスとなり、ライブラリ開発者にサポートを追加する時間が与えられていますが、Active LTS となってもライブラリ側でサポートが完了していない場合もあるため、注意が必要です。

2. TypeScript 直接実行に移行可能か確認

移行対象の TypeScript スクリプトを直接実行できるか確認します。後述する tsconfig のオプション変更が必要で、noEmit オプションの有効化や、import文での.ts拡張子を明示的に記述する必要があります。そのため、プロダクション環境で実行するコード群と開発で用いるTypeScript スクリプトが同じ package に入っていると、かなり整理がややこしくなり、移行が困難になります(詳細は後で紹介する別記事に譲ります)。

HireRoo のフロントエンドでは、TypeScript のツール群はそれぞれが責務ごとに独立したパッケージとして開発されていたため、とくに問題なく移行を進めることができました。 ツールチェインとして TypeScript のスクリプトを利用する場合、責務ごとに別 package で管理するのがおすすめです。

2. tsconfig の オプションの有効化

Node.js を対応バージョンにアップデートすることで動かせるようになりますが、TypeScript コードが対応していないコードだった場合、実行時のトランスパイルのタイミングでエラーが発生してしまいます。これらを事前にチェックするために、tsconfig の オプションを有効化します。

erasableSyntaxOnly

TypeScript 5.8 で導入された erasableSyntaxOnly オプションを有効化します。このオプションは、JavaScript でサポートされていない TypeScript 固有の構文をエラーとして検知するようにするもので、Node.js と TypeScript の互換性を高めることを目的としたものです。Node.jsでTypeScriptの型注釈を削除し、そのまま実行できるようになります。 例えば、enuminterfaceなどの TypeScript 固有の構文がエラーとして検知されるようになります。

enum Hoge {
  HOGE = 'hoge',
}

HireRoo では一部クラスプロパティの書き方を TypeScript の構文で記述していたため、修正するといった軽微な変更に留まりました。

export class Docker {
  constructor(private kind: DockerImageKind) {}
  ...
}

// ↓↓↓↓↓ 移行後 ↓↓↓↓↓
export class Docker {
  private kind: DockerImageKind;
  constructor(kind: DockerImageKind) {
    this.kind = kind;
  }
  ...
}

allowImportingTsExtensions

TypeScript 5.7 で導入された allowImportingTsExtensions オプションを有効化します。このオプションは、.tsなどのTypeScript の拡張子を import 文で使用できるようにするものです。 前提として、Node.js での直接実行は、ESM向けのモジュール解決を前提とするため、相対パスでのimport宣言では拡張子を省略できません。ランタイム時にそのまま解決できる.jsを明示的に記述する必要があります。 一方、開発時にTypeScript のコード内では、.ts の拡張子を使用します。これらを両立させるために、allowImportingTsExtensions オプションを有効化します。

import { hoge } from './hoge';

// ↓↓↓↓↓ 移行後 ↓↓↓↓↓

import { hoge } from './hoge.ts';

ただし、このオプションは noEmit オプションと同時に有効化する必要があります。有効化に伴う注意点がいくつかあるため、利用にあたってはTS 5.7の –rewriteRelativeImportExtensions オプションを使う前に読む記事を参考に、どのような影響があるかを確認しておくことをお勧めします。

4. 動作確認

これらの準備が整ったら、ts-node の代わりに node を使用して TypeScript ファイルを実行します。プロジェクトでは、各パッケージの package.json に定義されていた実行スクリプトを、node --loader ts-node/esm --experimental-specifier-resolution=node から node に変更しました。移行後は、コード生成スクリプトやバリデーションスクリプトなど、すべての TypeScript スクリプトが正常に動作することを確認しています。

補足: node_modules に依存したスクリプトは、Node.js での直接実行に移行できない

今回の移行では、スタンドアロンで動作するコマンドラインツールやスクリプトのみを対象としました。node_modules 内のパッケージに依存しているスクリプトや、バンドルが必要なアプリケーションコードは、Node.js での直接実行に移行できません。これらのケースでは、従来通りトランスパイルやバンドルのプロセスが必要です。

これらができるようにあるためには、TypeScript のソースコードを node_modules に展開されている必要がありますが、以下のような問題が発生することから難しいとされています。Node.js の見解としては、現状 npm に公開されるパッケージは .ts のままでなく .js にビルド済みであるべきとしています。

  • Node.js が実行できないファイルが依存として入る
  • 実行時に TypeScript トランスパイル環境を要求してしまう
  • 依存関係が複雑化する
  • エコシステムの統一性が壊れる

参考:https://nodejs.org/docs/latest-v22.x/api/typescript.html#type-stripping-in-dependencies

おわりに

今回の移行により、依存関係が簡素化され、開発環境がよりシンプルになりました。Node.js が TypeScript を直接サポートするようになったことで、サードパーティへの依存を一つ減らすことができました。

ハイヤールーでは、CTO の shogosensui が主導で、このような開発・CI/CD環境・コーディング体験の改善を継続的に進めています。今回の ts-node からの移行は、その一環として実施されました。ハイヤールーはコーディング試験サービスを軸とした開発者向けサービスを展開しています。最新の技術動向にアンテナを貼っておくと、結果的にエンドユーザーとなる開発者と、サービスを作る側としての開発者体験が両方向上することもあるユニークなドメインです。今後も、開発体験の向上とプロダクト開発の推進を両立していきます。