TypeScript のコンパイルを理解する
🌼 はじめに
こんにちは、にわかエンジニアです。
私はエンジニア歴のほとんどを TypeScript と一緒に過ごしていてあまり気づいてなかったのですが、どうやら TypeScript のコンパイルは他の言語に比べて変わってるらしいです。
なので、TypeScript のコンパイルについて調べたものを共有したいと思います!
1. コンピューターの言語、人の言語
まずは根本的な言語(とその翻訳)について説明します。
1-1. 低水準言語
低水準言語(low-level language)は、コンピュータが理解しやすいように作られた言語で、一般的に機械語とアセンブリ言語を指します。
機械語(machine language)は CPU が直接実行する0と1の列です。
01001000 11000111 11000000 00000001 00000000 00000000 00000000
01001000 10000011 11000000 00000010
11000011
私が学生時代によく聞いてた曲[1]に0と1で構成されたデジタルがなんとかの歌詞がありましたが、それって機械語のことだったかーと今になって思います。
アセンブリ言語(assembly language)は、機械語を多少は人が読めるようにした言語です。先の機械語の例をアセンブリ言語で表現してみました。
mov rax, 1
add rax, 2
ret
mov(move)、add、ret(return)など自然言語が入って先よりはちょっとわかる気がします。
昔は人が機械語でプログラミングしないといけなく、それが厳しすぎて多少人に優しくなったアセンブリ言語が登場したようです。
その後人がもっと効率的にプログラミングするために高水準言語が登場します。
1-2. 高水準言語
高水準言語(High-level language)は、人が理解しやすい自然言語に近づけて作られた言語です。
例で偶数と奇数をトータルで10回出力しているC言語のコードを見てみましょう。
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) printf("%d: even\n", i);
else printf("%d: odd\n", i);
}
return 0;
}
低水準言語に比べたらとてもわかりやすくなりました。
このように高水準言語は読みやすく、操作が簡単なという利点があります。現代はほとんど高水準言語で開発しているでしょう。
ですが、プログラムを実行する主体は人じゃなくて機械なので、人が書いた高水準言語を機械がわかるように低水準言語に翻訳してあげる必要があります。
その「高水準言語 → 低水準言語」の翻訳のやり方がコンパイル方式とインタプリタ方式です。
1-3. コンパイル
コンパイルとは、プログラム全体を事前に翻訳して実行ファイルを作成する方式です。この翻訳を行うプログラムをコンパイラと呼びます。
コンパイル方式で翻訳する言語は C/C++、Rust、Go などがあります。C言語で例をあげてみましょう。
// hello.c
#include <stdio.h>
int main(void) {
int a = 1, b = 2;
printf("Hello, world! sum=%d\n", a + b);
return 0;
}
hello.c
を実行する手順は、①gcc
コンパイラで実行ファイルを作成し、②作成された実行ファイルを実行する、という流れです。
# hello.c を hello という実行ファイルに翻訳
gcc hello.c -o hello
# 実行ファイルを実行
./hello
コンパイル後すべてのコードが機械語に翻訳済みであるため、処理速度が速いというメリットがあります。
ただ、実行前にコンパイル必要だから初回実行まで時間かかる(+ ビルド時間かかる)というデメリットもあります。
1-4. インタプリタ
インタプリタ方式とは、プログラムを1行/1ブロックずつ翻訳しながら実行する方式です。この翻訳と実行を同時に行うプログラムをインタプリタと呼びます。
インタプリタ方式で翻訳する言語は Python、Ruby、PHP などがあります。Python で例をあげてみましょう。
# hello.py
a, b = 1, 2
print(f"Hello, world! sum={a + b}")
hello.py
は事前翻訳なしですぐ実行できます。
# 実行
python hello.py
インタプリタ方式は実行前の翻訳過程が不要なので、コードを修正してもすぐ実行できるというメリットがあります。
ただ、実行しながら翻訳する必要があるのでコンパイル方式と比べたら実行に時間がかかる場合があります。
2. TypeScript のコンパイルと実行
では具体的に TypeScript がどうやってコンパイルされるか見ていきましょう。
2-1. TS ファイル作成
まずは簡単に挨拶を出力するプログラムを TypeScript で書いてみました。
type Lang = 'ko' | 'ja' | 'en';
type User = {
name: string;
lang?: Lang;
};
const GREETINGS = {
ko: '안녕하세요',
ja: 'こんにちは',
en: 'Hello',
} as const
function greet(user: User): string {
const lang: Lang = user.lang ?? 'en';
return `${GREETINGS[lang]}, ${user.name}!`;
}
(function main() {
const me: User = { name: 'TS', lang: 'en' };
console.log(greet(me));
})();
2-2. コンパイル
書いた TS ファイルを公式の TypeScript コンパイラー(tsc
)でコンパイルします。
# TS ファイルをコンパイル
npx tsc src/hello.ts
tsc
の仕事は大きく①型チェック、②JSへの変換の2つあります。
まず①型チェック段階でエンジニアが書いたコードに型エラーがないか検査します。確認のために先ほど作成した hello.ts
で型エラーを発生させてみます。
type Lang = 'ko' | 'ja' | 'en';
type User = {
name: string;
lang?: Lang;
};
const GREETINGS = {
ko: '안녕하세요',
ja: 'こんにちは',
// en: 'Hello',
} as const
function greet(user: User): string {
const lang: Lang = user.lang ?? 'en';
return `${GREETINGS[lang]}, ${user.name}!`; // TS7053: Element implicitly has an any type because expression of type Lang can't be used to index type
}
(function main() {
const me: User = { name: 'TS', lang: 'en' };
console.log(greet(me));
})();
この状態で tsc
を実行すると以下のようなエラーを出力されます。
src/hello.ts:16:15 - error TS7053: Element implicitly has an 'any' type because expression of type 'Lang' can't be used to index type '{ readonly ko: "안녕하세요"; readonly ja: "こんにちは"; }'.
Property 'en' does not exist on type '{ readonly ko: "안녕하세요"; readonly ja: "こんにちは"; }'.
16 return `${GREETINGS[lang]}, ${user.name}!`;
~~~~~~~~~~~~~~~
Found 1 error in src/hello.ts:16
エラー確認できたのでコードを元にもどして型エラーを解消しました。
次の②JSへの変換段階では TypeScript ファイルを JavaScript ファイルに変換します。
大事なことなので2回言います。TypeScript コンパイラーは、TypeScript コードを JavaScript コードに変換します。
先ほど書いた TS ファイルを JS ファイルに変換したら以下のようになります。
const GREETINGS = {
ko: '안녕하세요',
ja: 'こんにちは',
en: 'Hello',
};
function greet(user) {
const lang = user.lang ?? 'en';
return `${GREETINGS[lang]}, ${user.name}!`;
}
(function main() {
const me = { name: 'TS', lang: 'en' };
console.log(greet(me));
})();
型が全部消えたプレーンな JavaScript になりました。
一般的には「コンパイル」と言うと高水準言語を低水準言語に翻訳することを意味しますが、TypeScript のコンパイル結果はまだ全然高水準である JavaScript です。これが TypeScript コンパイルの変わったところでしょう。
TypeScript でいうコンパイルは「型チェック」+「JSへの変換」であり、「高水準 → 低水準」ではありません。
なので「いや厳密に言うとコンパイルじゃないじゃん」と思うかもしれませんが、公式がコンパイルって言ってるのでコンパイルということになってる気もします。
https://www.typescriptlang.org/docs/handbook/2/basic-types.html#tsc-the-typescript-compiler
2-3. JS ファイル実行
コンパイル結果の JavaScript ファイルを実行してみましょう。
node src/hello.js
Hello, TS! # 挨拶してくれる
こうやって TypeScript は JavaScript にコンパイルされ、実行されます。
+)JavaScript はインタプリタ言語?
JS ファイルをすぐ実行できたので JavaScript はインタプリタ方式を採用しているでしょう。
しかし、実は現代の JavaScript はインタプリタ&コンパイル両方やります。
ざっくり以下の歴史的な流れで両方やるようになりました。
- 元々 JavaScript はインタプリタ言語
- WEBの進化が激しく、どんどんリッチなUIが登場
- インタプリタだけだと実行が遅い!
- インタプリタで即実行しつつ、よく走る箇所はJITコンパイル(実行中にコンパイル)して性能改善
+)TSを書いても実行されるのは JS ファイルということに気をつけましょう
実行時は型情報が全部消えるので、想定してた型と実際の型が違う場合もすぐ気付けないことがあります。
例えば開発時は Product
のような型を期待していたのに、
type Product = { amount: number };
const response = await fetch("/api/product");
const product: Product = JSON.parse(response);
console.log(product.amount + 1);
現実世界の型は違くて、
{
"count": "42" // number じゃなくて string
}
予期せぬ動きになってもエラーにはならないのですぐは気付けないです。
console.log(product.amount + 1); // 43 を期待したのに "431" になる
Rust、Kotlin など他の言語は期待してた型と違う時点でエラーをスローするからすぐ気づくらしいですね。TypeScript でも zod
などスキーマ検証すれば同じことはできますが、外部ライブラリーが必要になります。
3. ビルドとランタイムの疎結合
ではもう一度 TypeScript のコンパイルと実行を時系列に整理しえみましょう。
ここに一つ特異点があります。実行時は JS ファイルさえあれはいいので、TypeScript とか、ビルドの時に起きたこととかはどうでもよくなります。
この場合ビルドタイムとランタイムが疎結合(Loose Coupling)だと言えるでしょう。これがまた TypeScript の変わった部分です。
そういう特性なため、TypeScript はビルド時のツール入れ替えができます。具体的には、トランスパイル(TS→JS)段階で公式のコンパイラー(tsc
)じゃなく別のツール(SWC、esbuild など)を使えます。
しかし、トランスパイルは別のツールもできるけど、正確な型チェックは tsc
しかできないのが現状です。
ということでトランスパイルは別のツール使って早くしたのに型チェックには tsc
使う、というツール混在状態になり、使い勝手が微妙になることがあります。
4. TSGO 出るってよ
TypeScript 公式のコンパイラー(tsc
)は TypeScript で書かれていて、それが遅いです。
ですが、2025年3月11日 Microsoft が TypeScript のコンパイラを GO に書き換えて10倍早くすると発表しました。ムネアツですね。
TSGO がリリースされたら TypeScript のコンパイル(型チェック + トランスパイル)が爆速になり、別ツール使わず公式コンパイラだけで完結できてすっきりするんじゃないかなと思ってます。
正式リリースはまだですが、プレビューは公開されました。早くリリースしてほしいなと思う日々を送っています。
🌷 終わり
TypeScript コンパイルまわりだいぶふわっとしてましたが、これをきに色々理解できてよかったです。
-
調べたら EXO の MAMA という曲だった ↩︎
Discussion