トランスパイラ

ある言語のソースコードを別の言語や同じ言語の異なるバージョンに変換するツール

ビルド言語

トランスパイラとは

トランスパイラ (Transpiler / Source-to-Source Compiler) は、あるソースコードを別の言語、または同じ言語の異なるバージョンへ変換するツールである。TypeScript から JavaScript、JSX から JavaScript、新しい ECMAScript から ES5 への変換が代表例。

コンパイラーと対立する概念ではない。字句解析 / 構文解析 / 変換 / 出力という工程はコンパイラーと同じで、違うのは出力の抽象度だけである。出力が機械語やバイトコードではなく人が読める高水準言語であるコンパイラーを、慣習的にトランスパイラと呼び分けているにすぎない。コンパイラーの部分集合と捉えるのが正確で、両者を並列に対比させると変換器の設計上の制約 (後述) が見えなくなる。

用途は 3 つに分けて考えると混乱しない。

  1. 実行環境が持たない言語機能を取り除く (型注釈や JSX を素の JavaScript へ)
  2. まだ解釈できない新しい構文を、古い構文へ書き換える (平準化)
  3. 実行環境に無い API を補う (ポリフィル)

「古いブラウザー向けの変換ツール」という理解は 2 だけを見たものである。現在の主用途は 1 に移っており、3 は変換ではなく実装の持ち込みなので、トランスパイラ単独では完結しない。

主要なツールと役割

ツール実装言語主な役割型チェック
tscTypeScript型チェックと JavaScript への変換する
BabelJavaScript新構文の平準化、JSX 変換、ポリフィル注入の指示しない
SWCRustTypeScript / JSX の変換しない
esbuildGo変換とバンドルしない

速度差はよく「実装言語の違い」で説明されるが、それだけではない。SWC は単一スレッドで Babel の 20 倍、4 コアで 70 倍という値を公式サイトに掲げている (SWC 自身が公開しているベンチマーク値 / 2026 年 8 月時点)。一方で tsc が遅いのは、型チェックのためにプロジェクト全体の型を解決する必要があり、ファイル単位の並列処理に分解できないからである。役割が違うものを速度だけで比べても代替にはならない。

単一ファイル変換という制約

型を見ない変換器の速さは、1 ファイルを他ファイルの情報無しに変換できる設計から来ている (並列化でき、ファイル単位のキャッシュも効く)。その代償として、import した名前が型なのか値なのかを判断できない。

import { User, createUser } from './user';

User が型で createUser が値だとしても、変換器は ./user を読まない限り区別できない。型だけの参照を消し忘れれば実行時に無用なモジュール読み込みが残り、消しすぎれば副作用目的の import が落ちる。そのため TypeScript では import type / export type で型のみの参照を書き手が明示する。Vite の公式ドキュメントも、型だけの import が誤ってバンドルへ含まれるのを避けるためこの構文を使うよう案内している (2026 年 8 月時点)。

他ファイルの定義値を呼び出し側へ展開する const enum のような機能も、同じ理由で単一ファイル変換とは噛み合わない。

型チェックが要るなら、変換とは別のプロセスに分けるのが定型である。開発中は tsc --noEmit --watch、CI ではビルド前に tsc --noEmit を通す。

型注釈の除去と、除去されない構文

入力 (TypeScript):

interface User {
  id: string;
  name: string;
}

const greet = (user: User): string => `Hello, ${user.name}`;

出力 (JavaScript):

const greet = (user) => `Hello, ${user.name}`;

interface と型注釈は出力に残らない。ただし TypeScript の構文が全て消えるわけではない。列挙型 (enum) は実行時に存在するオブジェクトを生成し、数値列挙型では値から名前を引く逆マッピングまで出力される (TypeScript 公式ドキュメントに明記 / 2026 年 8 月時点)。型の世界だけで完結する記述と、実行時のコードを生む記述は区別しておく。後者は出力サイズを増やし、前述の単一ファイル変換の制約にも触れる。

構文の変換とポリフィルは別問題

構文は書き換えで古い形にできるが、環境に存在しない API (Promise, Array.from) は実装そのものを持ち込むしかない。ここの混同が設定ミスとして最も多い。

{
  "presets": [["@babel/preset-env", { "targets": "> 0.5%, not dead" }]]
}

この設定が行うのは構文の平準化だけで、ポリフィルは 1 つも入らない。API を補うには注入の指定が別に必要で、実体は core-js という別ライブラリである。しかも指定方法は Babel の系列で変わる。preset-env に注入オプションを渡す形は Babel 8 で削除され、専用プラグイン (babel-plugin-polyfill-corejs3) を直接使う形になった (2026 年 8 月時点の Babel 公式ドキュメント)。系列を跨いで設定例を写すと、ポリフィルが入っていないことに気づかないまま古い環境で落ちる。

そもそもポリフィルで埋められない範囲もある。任意のプロパティアクセスを捕捉する Proxy のような言語機能は、対象を事前に列挙しない限り後付けで再現できない。ダウンレベル変換自体にも代償がある。async/await をジェネレーターとランタイムへ展開すれば、出力量と実行時のオーバーヘッドが増える。

JSX の変換

入力:

const element = <button onClick={handleClick}>送信</button>;

古い変換方式 (classic):

const element = React.createElement('button', { onClick: handleClick }, '送信');

現在の既定 (automatic):

import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('button', { onClick: handleClick, children: '送信' });

automatic では変換器がランタイムを自動で import するため、JSX を書くファイルで React を import する必要がない。逆に、この方式を前提にしたコードを classic 用の設定で変換すると React is not defined になる。

開発時と本番で構成が変わる

[開発] ソース → 型を見ない高速な変換器 → ブラウザ (即時反映)
                 └ 型チェックは別プロセス (tsc --noEmit --watch)

[CI]   ソース → tsc --noEmit で型チェック → 通ればビルド
                                             (変換 + バンドル + 最小化)

Vite はこの形の代表で、TypeScript の変換に Oxc Transformer を使い、型チェックは行わない (公式ドキュメントに明記 / 2026 年 8 月時点)。内部で使う変換実装は版によって入れ替わってきたので、「Vite は◯◯を使う」という前提の設定情報は自分が使う版で確かめる。また tsconfig の target は無視されるため、出力の言語水準は変換器側の設定で指定する。バンドルとの関係は バンドラー を参照。

Lambda へのデプロイ

AWS SAM の esbuild ビルドメソッドを使えば、TypeScript を事前ビルドの成果物に頼らず直接デプロイできる。ここでも型チェックは挟まらないので、CI で tsc --noEmit を別に通しておく。型の誤りがそのままデプロイされる経路を作らないことが要点である。

つまずきやすい点

  • 型を見ない変換器へ乗り換えてビルドが速くなった代わりに、型エラーが本番まで通る構成になっている
  • targets を書けばポリフィルが入ると思い込む (構文変換と API の補完は別工程)
  • 型だけの import に import type を付けず、変換器の判断に委ねてしまう
  • const enum や名前空間など、実行時の実体を伴う機能を単一ファイル変換の前提で使う
  • ビルドツールが tsconfig の一部設定を見ないことを知らずに、出力の言語水準を誤る

言語仕様そのものは TypeScript を参照。

共有:

この記事は役に立ちましたか?

関連用語

関連する記事