tsc が何をして、何をしないのかを説明できるJavaScript には、書いた時点で間違いを教えてくれる仕組みがありません。次のコードはエラーを出さずに動きます。
// JavaScript
function greet(user) {
return `Hello, ${user.name}`;
}
console.log(greet({ namae: "Alice" })); // "Hello, undefined"
namae は name の打ち間違いですが、JavaScript は存在しないプロパティを読んでも undefined を返すだけです。例外も出ず、それらしい文字列が出力されます。この出力を誰かが見つけるまで、間違いは残り続けます。
同じコードに型を付けると、実行する前に止まります。
// TypeScript
type User = { name: string };
function greet(user: User) {
return `Hello, ${user.name}`;
}
console.log(greet({ namae: "Alice" }));
// ~~~~
// error TS2353: Object literal may only specify known properties,
// and 'namae' does not exist in type 'User'.
//
// 訳: オブジェクト リテラルは既知のプロパティのみ指定できます。
// 'namae' は型 'User' に存在しません。
エラーメッセージは既定で英語です。
この資料では実際に出力される英語をそのまま載せ、日本語訳を添えます。訳は tsc --locale ja の出力に合わせてあります。エラー番号(TS2353 など)で検索する場合、英語のほうが情報が見つかります。
どの言語でも打ち間違いはします。JavaScript に固有なのは、間違いが実行時まで表面化せず、しかも実行しても例外にならない場合があることです。
| ありがちな間違い | JavaScript での結果 | 気づけるか |
|---|---|---|
| プロパティ名の打ち間違い | undefined が返る | 気づけない |
| 引数の数を間違える | 足りない引数は undefined | 気づけない |
| 文字列と数値を取り違える | "1" + 1 が "11" になる | 気づけない |
null のプロパティを読む | 実行時に例外 | 実行時のみ |
| 関数名の打ち間違い | 実行時に例外 | 実行時のみ |
上の3行はテストを書いても見逃す可能性があります。値が「それらしく」出てしまうためです。TypeScript が解決しようとしたのは、この「気づけなさ」です。
Java・C# から来た人へ
この章の内容は「型がないとこうなる」という当たり前の話に見えるはずです。読み飛ばして 01.2 へ進んで構いません。ただし、TypeScript の型が Java・C# の型とまったく別の仕組みで動いていることは 01.2 と 01.3 で扱います。そこは飛ばさないでください。
TypeScript のコンパイラ tsc がやることは、実質次の2つだけです。
tsc は型を検査し、型を取り除いた JavaScript を出力する次の .ts をコンパイルすると、
// greet.ts
type User = { name: string; age: number };
export function greet(user: User): string {
return `Hello, ${user.name}`;
}
出力される .js はこれだけです。
// greet.js — 型に関する記述がすべて消えている
export function greet(user) {
return `Hello, ${user.name}`;
}
type User の定義も、引数の : User も、戻り値の : string も残りません。型は開発中にだけ存在する情報です。
この一点が、以降のすべての章に効いてきます。
型が実行時に消えるため、外から入ってくる値(JSON・環境変数・API のレスポンス)が型どおりである保証はどこにもありません。型を書いても、実行時の検証は別途必要です。この扱いは 11. 非同期とエラー処理 と A3. 実務パターン集 で具体的に扱います。
意外に思われる点ですが、tsc は型エラーを報告しても JavaScript の出力は行います(既定の設定の場合)。型検査と出力は独立した処理だからです。
$ npx tsc greet.ts
greet.ts:7:21 - error TS2353: Object literal may only specify known properties,
and 'namae' does not exist in type 'User'.
7 console.log(greet({ namae: "Alice" }));
~~~~
Found 1 error in greet.ts:7
$ ls
greet.js greet.ts ← エラーがあっても .js は出力されている
止めたい場合は noEmitOnError を有効にします。設定の詳細は 03. tsconfig で扱います。
tsx を使うと、コンパイルの手順を意識せずに .ts をそのまま実行できます。
// hello.ts
const name: string = "TypeScript";
console.log(`Hello, ${name}!`);
$ npx tsx hello.ts
Hello, TypeScript!
Node型ストリップ 新しい Node であれば、tsx を使わずそのまま実行することもできます。Node が型注釈を取り除いてから実行するためです。
$ node hello.ts
Hello, TypeScript!
どちらの方法にも制約と使い分けがあります。詳細は 02. 実行環境 で扱います。
Java や C# の経験がある読者ほど、ここで誤解が起きます。どちらもコンパイルと呼びますが、出力されるものも、型情報の行き先も違います。
| 観点 | Java / C# | TypeScript |
|---|---|---|
| 出力 | バイトコード / IL(人が読むものではない) | JavaScript(人が読める。元のコードとほぼ同じ形) |
| 型情報 | 実行時まで残る | 実行時には残らない |
| 型エラー時 | ビルドが失敗し、成果物ができない | エラーを報告するが、出力はされる(既定) |
| 型の判定基準 | 名前で判定する(名前的型付け) | 形で判定する(構造的型付け → 05) |
| 実行するもの | JVM / CLR | JavaScript エンジン(Node・ブラウザ) |
Java・C# から来た人へ
Java の instanceof や C# の is にあたる判定を、TypeScript の interface に対しては書けません。interface は実行時に存在しないためです。
interface User { name: string }
// 書けない — User は実行時に存在しない
// if (x instanceof User) { ... }
// ~~~~
// error TS2693: 'User' only refers to a type, but is being used as a value here.
//
// 訳: 'User' は型のみを参照しますが、ここで値として使用されています。
実行時に「これは User か」を判定するには、自分で判定コードを書きます。その方法は 08. 型の絞り込み で扱います。
Java の getClass()、C# の typeof(T) のように実行時に型情報を取り出す仕組みは TypeScript にありません。ジェネリクスの型引数も実行時には消えます(09. ジェネリクス)。
この制約は不便に見えますが、理由は単純です。TypeScript は JavaScript に何も足さないためです。出力が普通の JavaScript である以上、JavaScript にない機能を実行時に持ち込むことはできません。
「JavaScript に何も足さない」は設計方針です。
TypeScript は、実行時の挙動を変える機能を原則として追加しません。これにより、既存の JavaScript をそのまま .ts にリネームしても動くという性質が保たれ、段階的な導入が可能になっています。enum のように、この方針の例外にあたる機能もあります(04. 型の基本)。
「JavaScript は実行するまで間違いに気づけない」という問題への答えは、TypeScript だけではありません。
.js のままコメントで型を書き、tsc に検査だけさせる方法です。ビルドの手順が増えないのが利点です。
// greet.js — 拡張子は .js のまま
/**
* @param {{ name: string }} user
* @returns {string}
*/
function greet(user) {
return `Hello, ${user.name}`;
}
| 選ぶ基準 | 向いている方 | 理由 |
|---|---|---|
| 新規のプロジェクト | TypeScript | 記述量が少なく、型の表現力も高い |
| ビルド手順を増やせない | JSDoc | .js のまま検査できる |
既存の大きな .js 資産がある | JSDoc から | ファイルを変換せず、1ファイルずつ型を足せる |
| 使い捨てのスクリプト | 型なし | 型を書く手間が見合わない場合がある |
この資料は TypeScript(.ts)を前提に進めます。
JSDoc で書く場合も、型の考え方は同じです。書き方が違うだけなので、以降の章の内容はそのまま使えます。
かつて TypeScript と競合した型システムに Flow があります。現在の新規プロジェクトで選ぶ理由はほとんどありませんが、古いコードで // @flow というコメントを見かけたら Flow のコードだと判断できれば十分です。
まとめ
| 項目 | この章の結論 |
|---|---|
| 解決したい問題 | JavaScript は間違いに実行時まで気づけない。しかも例外にならず、それらしい値が出てしまう |
tsc の仕事 | 型を検査することと、型を取り除いた JavaScript を出力することの2つだけ |
| 型の寿命 | 開発中のみ。実行時には残らない |
| Java・C# との違い | 出力が JavaScript であること、型情報が実行時に消えること、型エラーでも出力されること |
| 他の選択肢 | JSDoc なら .js のまま検査できる。型の考え方は同じ |
次は 02. 実行環境 です。tsc・tsx・Node の型ストリップの違いを押さえ、手元で .ts が動く状態を作ります。