1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

型はどこへ消えた? TypeScript が JavaScript に化けるまでの全工程

1
Last updated at Posted at 2026-06-09

はじめに:tsc って結局、中で何してるの?

.ts ファイルを書いて tsc を叩くと、型がきれいさっぱり消えた .js が出てきます。毎日やっている操作ですが、あの一瞬で中身では何が起きているのか、ちゃんと説明できるでしょうか?

この記事では、TypeScript が JavaScript に「化ける」までの工程を、実際の変換結果を見ながら追いかけます。先に結論だけ言っておくと、こうです👇

トランスパイルの本体は「型を消す」+「古い構文を書き換える」だけ。型チェックは、実は別の仕事。

この一文がピンとこない人ほど、最後まで読むと世界が変わります。ついでに2026年の二大事件(Node が自分で型を剥がし始めた件と、tsc 本体が Go に引っ越した件)まで一気に駆け抜けましょう。

動作確認や用語は2026年6月時点のものです。バージョン番号の進みが速い領域なので、最新情報は末尾の公式リンクで確認してください。


大前提:tsc は「二刀流」だった

多くの人が「TypeScript のコンパイル = 型チェック」だと思っています。半分正解で、半分は誤解です。

tsc の仕事は、実はまったく独立した2つに分かれています。

  1. 型チェック(type checking) … 型が合っているかを検査してエラーを出す
  2. コード生成(emit) … 型を消し、古いブラウザでも動く構文に書き換えて .js を吐く

ここが今日いちばん大事なポイントです。②のコード生成は、①の型チェックを必要としません。 型注釈を消して構文を書き換えるだけなら、型が合っているかどうかは関係ないからです。

この「分離」を知っていると、後半で出てくる esbuild / swc / Node が「なぜ型チェックなしで一瞬で変換できるのか」がスッと理解できます。逆にこれを知らないと一生モヤモヤします。


全体図:ソースが .js になるまでの工程

まずは地図から。tsc の内部は、大きく 6つの段階 に分かれています。ソースコードが、段階ごとに別の「データのかたち」へと姿を変えていくイメージです。

ポイントは Checker(型チェック)が脇道に逸れていること。型チェックは「型エラーの報告」という別の出口を持っているだけで、生成される .js には直接影響しません。点線がその「別レイヤー感」です。

後で出てくる爆速トランスパイラたちは、この Checker をまるごとスキップして D → F → H だけ(むしろ B → 簡易変換 → H)を走ります。だから速い。覚えておいてください。


各工程を実況中継

例として、こんな1行を流してみます。

const msg: string = "Hello";

① Scanner(字句解析器):文字をトークンに切り刻む

ソースを1文字ずつ読み、意味のある最小単位「トークン」の列に分解します。

const  →  ConstKeyword
msg    →  Identifier
:      →  ColonToken
string →  StringKeyword
=      →  EqualsToken
"Hello"→  StringLiteral
;      →  SemicolonToken

② Parser(構文解析器):トークンから AST を組み立てる

トークン列を読んで、木構造の AST(抽象構文木) を作ります。各ノードは SyntaxKind という enum で種類が識別されます。たとえば a + 1 なら、Identifier(a)PlusTokenLiteralExpression(1) を子に持つ BinaryExpression になります。

VariableStatement
└─ VariableDeclaration
   ├─ name: msg
   ├─ type: StringKeyword   ← この型情報、後で消されます
   └─ initializer: "Hello"

③ Binder:名前と宣言を結びつける

AST を一巡して シンボルテーブル(識別子 ↔ 宣言の対応表)を作り、スコープを構築します。ここが地味に重要で、Binder はこのとき各ノードに「これはどの ES バージョンの構文か(ES2015 固有なのか、TypeScript 固有なのか)」という印もこっそり付けます。これが後段の変換の下準備になります。

④ Checker(型チェッカー):TypeScript の心臓部

AST とシンボルを使って、型推論・型互換性チェック・ジェネリクス解決・制御フロー解析(ナローイング)などを行い、型エラーを報告します。number に文字列を代入していたら怒るのはここの仕事です。

TypeScript のソースコードの中でもダントツに巨大なコンポーネントで、TypeScript の「賢さ」のほぼ全部がここに詰まっています。そして繰り返しますが、この工程は emit には本質的に不要です。

⑤ Transformer:型を消し、古い構文に書き換える

ここが「化ける」瞬間です。TypeScript 変換は基本的に型注釈をポイっと捨てるだけ。ただし target が古い場合は、新しい構文を古い JavaScript で実装し直すコードを生成します(例:ES2015 → ES5)。新しい方言から古い方言へ、段階的にダウンレベルしていくイメージです。

⑥ Printer / Emitter:AST を文字列にして書き出す

変換後の AST を、最終的な JavaScript の文字列として出力します。.js と同時に、.d.ts(型定義ファイル)やソースマップもここで生成されます。

最終的に、最初の1行はこうなります。

const msg = "Hello";

型が消えただけ。シンプルですね。……が、世の中はこんなに平和な例ばかりではありません。


「型消去」が変換の本体

TypeScript には、こういう設計原則があります。

型は、実行時の振る舞いに一切影響しない。

だから interfacetype、型注釈は、出力 JS から完全に消滅します。痕跡すら残りません。

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

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

↓ コンパイル後

const greet = (u) => `Hi, ${u.name}`;

interface User は影も形もありません。これが「型消去(type erasure)」です。

ただし例外がありますenum / namespace / (レガシーな)デコレータ / クラスの「パラメータプロパティ」などは、実行時のコードを生成します。つまり「ただ消すだけ」では済まない、TypeScript の中の少数派です。この少数派が、のちのち大事件を引き起こします。


実録:あなたのコードはこう化ける

ここからが本記事の見せ場。「消えるだけ」じゃない連中の、衝撃のビフォーアフターを見ていきましょう。

ケース1:enum — 伝説の IIFE 逆引きマッピング

何気なく書いたこの enum 👇

enum UserRole {
  Admin,   // 0
  Editor,  // 1
  Viewer,  // 2
}

コンパイル後、こうなります。

"use strict";
var UserRole;
(function (UserRole) {
  UserRole[UserRole["Admin"] = 0] = "Admin";
  UserRole[UserRole["Editor"] = 1] = "Editor";
  UserRole[UserRole["Viewer"] = 2] = "Viewer";
})(UserRole || (UserRole = {}));

初見だと「は?」となるやつです。これは即時実行関数(IIFE)で、双方向のマッピングを作っています。UserRole[UserRole["Admin"] = 0] = "Admin" という1行で、

  • UserRole["Admin"]0(名前から値)
  • UserRole[0]"Admin"(値から名前、これを reverse mapping と呼びます)

の両方を同時に仕込んでいるわけです。公式ハンドブックでも、numeric enum は「名前→値」と「値→名前」の両方を持つオブジェクトにコンパイルされる、と明言されています(string enum には逆引きは生成されません)。

「enum はやめて as const を使え」という意見をよく見かけるのは、このランタイムへの重さと、interface のように消えてくれない非対称さが理由です。

const UserRole = {
  Admin: 0,
  Editor: 1,
  Viewer: 2,
} as const;
type UserRole = (typeof UserRole)[keyof typeof UserRole];

こちらは素の JavaScript に素直に化けるので、tree-shaking もしやすく、後述の「型を剥がすだけ」のランタイムでもそのまま動きます。

ケース2:クラスの「パラメータプロパティ」

コンストラクタ引数に修飾子を付けるあのショートハンドも、ただの構文糖です。

class Person {
  constructor(private name: string, private age: number) {}
}

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

private が消え、代入文に展開されます。型は消えるけど、代入というランタイム動作が増えるのがミソ。

ケース3:async/await を ES5 に落とす — ステートマシン爆誕

ここが個人的に一番ロマンがあります。target を古い ES5 にすると、async/awaitジェネレータを模したステートマシンに変換されます。

async function foo() {
  let result = Math.random();
  if (result < 0.8) return "Recursion: " + await foo();
  else return "Result: " + result;
}

↓ ES5 ターゲット時のemit(概形)

function foo() {
  return __awaiter(this, void 0, void 0, function () {
    var result, _a;
    return __generator(this, function (_b) {
      switch (_b.label) {
        case 0:
          result = Math.random();
          if (!(result < 0.8)) return [3 /*break*/, 2];
          _a = "Recursion: ";
          return [4 /*yield*/, foo()]; // ← ここで「中断」
        case 1:
          return [2 /*return*/, _a + _b.sent()]; // ← 戻ってきたら続きから
        case 2:
          return [2 /*return*/, "Result: " + result];
      }
    });
  });
}

注目は switch (_b.label) の部分。これがステートマシンそのものです。

  • return [4 /*yield*/, foo()] は「ここで処理を中断して制御を手放す(= await)」の意味
  • Promise が解決したら、label を進めて次の case から再開する

await のたびに関数を抜けて、後でその続きから入り直す——という「一時停止と再開」を、ただの数値ラベルと switch で再現しているわけです。これを支えるのが、コンパイラが注入する2つのヘルパー関数 __awaiter(Promise でラップ)と __generator(ステートマシン本体)です。

targetES2017 以上にすると、async/awaitネイティブ構文のまま出力され、このステートマシンは生成されません。つまり上記の変換は「古い実行環境のための互換コード」。__awaiter などのヘルパーは各ファイルにインライン展開されますが、importHelpers を付けると tslib から読み込む形にできて、出力が太るのを防げます。

まとめ表:消えるやつ・化けるやつ

機能 変換のされ方 ランタイムへの影響
型注釈 / interface / type 完全消滅 なし(純粋な型消去)
optional chaining a?.b(ES2019以下) 条件分岐に展開 構文ダウンレベル
async/await(ES5) __awaiter + ステートマシン ヘルパー注入
パラメータプロパティ コンストラクタ内の代入に展開 代入が増える
enum IIFE + 逆引きオブジェクト バンドルに残る
namespace IIFE バンドルに残る

だから「型チェックしないトランスパイラ」が世にあふれている

冒頭の伏線を回収します。型チェックと変換が分離できるからこそ、世の中には「型チェックはしないけど、型を消す作業だけは爆速」というツールが大量にあります。

  • Babel@babel/preset-typescript)… ファイル単位でパースして型を消すだけ。型チェックなし。
  • esbuild(Go製)/ swc(Rust製)… パース → 型ストリップ → 構文変換のみ。型解決をしないので1ファイルずつ独立に処理でき、これが爆速の源。

これらは型情報を使わないため、ファイルをまたいだ型の解決ができません。また .d.ts の生成には解決済みの型情報が必要なので、こちらは tsc(の API)の出番です。

そんなわけで、実務での定番はこの分業体制です👇

型チェックは tsc --noEmit に任せ、変換は esbuild / swc / Babel にやらせる。

型チェック コード生成 .d.ts 速度感 代表例
tsc / tsgo 遅い → Go で高速化 公式コンパイラ
esbuild / swc / Babel 爆速 バンドラ・ビルド系
Node 型ストリッピング △(剥がすだけ) 爆速・ビルド不要 ランタイム内蔵

事件①:Node が、自分で型を剥がし始めた

「トランスパイル時に何が動いているか」という観点で、2024〜2025年の大きな変化がこれです。Node.js 自体が TypeScript の型を剥がして、.ts を直接実行できるようになりました。

# Node 22.6+(実験的フラグ)
node --experimental-strip-types app.ts

# Node 23.6+ ではフラグ不要で実行できる
node app.ts

中で動いているのは Amaro というライブラリで、その正体は @swc/wasm-typescript(Rust 製 swc の WebAssembly 版)です。つまり Node は、内部に swc を抱え込んで型を剥がしているわけです。

おもしろいのが型の消し方。型注釈を「削除」するのではなく、空白に置き換えています。

const foo: string = "bar";

↓ 内部的にはこんな感じ(型のあった場所が空白に)

const foo         = "bar";

こうすると行・列のオフセットがズレないので、ソースマップなしでもスタックトレースが元コードと一致します。地味ですが賢い。

ただし、Node がやっているのは「型を剥がすだけ」で、型チェックは一切しません。型安全性の担保には別途 tsc --noEmit が必要です。

さらに、デフォルトの strip モードでは消すだけしかできないので、enumnamespace のようにコード生成が必要なやつは動きません。どうしても使うなら --experimental-transform-types が必要になります。……ほら、また少数派の enum が引っかかってきました。

事件②:enum、おまえだったのか —— erasableSyntaxOnly と「型はコメント」の未来

Node のように「剥がすだけ」のランタイムが主流になると、「消せる構文」と「消せない構文」の線引きが急に重要になります。

そこで TypeScript 5.8 に登場したのが --erasableSyntaxOnly フラグ。これを有効にすると、「消すだけでは済まない構文」をコンパイルエラーにしてくれます

// ❌ enum は消せない(コード生成が必要)
enum Direction { Up, Down }

// ❌ ランタイムコードを持つ namespace
namespace Container { export const x = 1; }

// ❌ パラメータプロパティ
class Point {
  constructor(public x: number, public y: number) {}
}

「消せる構文(erasable syntax)」だけで書け、という強制ギプスですね。verbatimModuleSyntax(型だけの import を import type で明示させる)と組み合わせるのが定番で、これが通れば「Node の型ストリッピングで確実に動く」というお墨付きになります。

そして、この流れの背景にはもっと大きな絵があります。TC39(JavaScript の標準化団体)で進む 「Types as Comments(型をコメント扱いする)」 提案です。

ブラウザが、TypeScript の型構文をコメントのように無視して実行できるようにする。

// 未来の JavaScript(?)
function add(a: number, b: number): number {
  return a + b;
}
// ブラウザ:「a: number ? 知らんけど無視して実行するわ」

これが実現すれば、ビルドステップなしで TypeScript がそのまま動く未来が来ます。提案のチャンピオンは TypeScript の PM、Daniel Rosenwasser 本人。ただし条件は「型が無視できること」。enum のように実際の JS を生成する構文は、この未来の蚊帳の外なのです。

整理すると、TypeScript はいま**「型はそもそも消すものだ」という思想**に大きく舵を切っています。enum が肩身の狭い思いをしているのは、こういう時代の流れがあるからなんですね。


事件③:2026年最大の衝撃 —— tsc 本体が Go に引っ越した

最後はコンパイラそのものの話。2026年、tscGo へ移植されました。コードネームは Project Corsa、コマンドは tsgo です。

ここまで「中で何が動いているか」を見てきましたが、その実装言語が JavaScript から Go に変わった、というのがこの事件の核心です。

タイムライン

  • TypeScript 6.0(2026年3月23日)… 最後の JavaScript ベース版。5系から7系への橋渡しリリース。
  • TypeScript 7.0 Beta(2026年4月21日)… Go ベースの新コンパイラ tsgo の公開ベータ。6.0 比でしばしば約10倍速いとされる。

なぜ速くなる?

公式(Anders Hejlsberg)いわく、10倍のうち半分はネイティブコード化、もう半分は共有メモリの並列処理から来ているとのこと。型チェックを複数コアでぶん回せるようになったのが大きい。

なぜ「作り直し」ではなく「移植」なのか

ここが面白いところ。チームはゼロからの再設計ではなく、関数単位の移植を選びました。理由は、巨大で複雑な型チェッカーには「そのコードの正確な挙動の中にしか書かれていない振る舞い」が大量にあるから。下手に作り直すと、微妙に挙動の違う「互換性のない TypeScript」が生まれて誰も使えなくなる——という判断です。だから新コンパイラは、過去のクセまで含めて旧来と同じように設計されています。

なぜ Go なのか(Rust じゃなく)

公開直後、ネットは「なぜ Go? なぜ Rust や C# じゃない?」で大荒れしました。TypeScript と C# の両方を生み出した Hejlsberg 本人が火消しに回るほど。要点はこうです。

  • Go:自動 GC を持ちつつ到達できる最も低レベルな言語。全プラットフォームでネイティブコード、データレイアウトの細かい制御、そして循環参照だらけのデータ構造を素直に扱える。
  • Rust 不採用:TypeScript コンパイラの内部データ構造は循環参照まみれで、Rust の所有権・借用チェッカーと根本的に相性が悪い。これを解きほぐすと移植が「年単位+互換性崩壊」コースになる。
  • C# 不採用:C# はバイトコード前提でネイティブ志向ではなく、コードベースが関数とstruct中心で OOP 的でないため、Go の方が移植しやすかった。

開発リードの Ryan Cavanaugh の言い回しが秀逸でした。要約すると「Rust は設計目標を見事に達成している。ただし『この特定の JS コードベースから移植しやすい』ことは、まったく合理的に、その設計目標に入っていなかった」。

内部アーキテクチャは旧来とほぼ同じで、internal/scanner / parser / binder / checker / printer といったパッケージ構成を踏襲しています。この記事で見てきた6段階の工程は、Go 版でもそのまま生きているわけです。

ただし注意点として、emit(JS出力)・--watch・API はまだ完成途上で、型チェックが主要な互換性に達している段階です。「型チェックだけ爆速でやりたい」用途なら今すぐ試せます。


手元で覗いてみよう

理屈がわかったら、自分の目で確かめるのが一番楽しいです。

変換結果を見る

  • TypeScript Playground … 左に .ts、右に emit 後の .jstarget を切り替えると enumasync の化け方が変わる様子を観察できます。
  • ts-ast-viewer.com … AST・シンボル・型・emit を全部可視化。Scanner〜Emitter の各段階を目で追えます。

コマンドで試す

# ES5 に落として、async/await のステートマシンを拝む
npx tsc --target ES5 app.ts && cat app.js

# 「消せる構文」だけで書けているかチェック
npx tsc --noEmit --erasableSyntaxOnly app.ts

# Go 版コンパイラ tsgo のプレビューを入れてみる
npm i -D @typescript/native-preview

# Node に直接 .ts を食わせる(22.18+ / 23.6+)
node app.ts

まとめ

長旅おつかれさまでした。持ち帰ってほしいのは、この3点です。

  1. トランスパイルの本体は「型消去 + 構文ダウンレベル」。型チェックはそれとは独立した別レイヤー。だから esbuild / swc / Node は型チェックを飛ばして爆速で変換できる。
  2. enum などの少数派は実行時コードを生成するinterface のように消えてくれないこの非対称さが、erasableSyntaxOnly や「Types as Comments」という**「型はそもそも消すもの」という時代の流れ**を生んでいる。
  3. 2026年、tsc 本体が Go(tsgo)に引っ越した。工程の中身は同じまま、実装言語を変えて約10倍速へ。循環参照だらけの設計ゆえに Rust ではなく Go が選ばれた、という裏話つき。

次に tsc を叩くとき、あの一瞬で Scanner がトークンを刻み、Parser が木を組み、型がふわっと消えて、enum がこっそり IIFE に化けている——そんな舞台裏を思い出してもらえたら嬉しいです。


参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?