TypeScript Design Goals
TypeScriptは、コンパイルによってJavaScriptへ変換されるAltJSの一種である。 近年、TypeScriptではネイティブ実装への移行が進められており、コンパイル速度も大幅に向上している。 そんな熱々のTypeScriptを、私は仕事や個人開発で使用している。 これまでTypeScriptを使う際には、堅牢なプログラムを作ることを意識してきた。しかし、TypeScriptで書かれたソースコードは堅牢にもできる一方で、そうではないコードを書くこともできる。
そこで私は、次のような疑問を持った。 「なぜ、言語として堅牢な型システムを提供しないのだろうか」 同じような疑問を持ったことがある人もいるのではないだろうか。
今回は、TypeScriptが何を目的として設計されているのかを見ていこうと思う。
なぜ、このような話を書こうと思ったのか
あるとき、tsconfig.json内の設定値の違いによって問題が発生した。
その設定がnoUncheckedIndexedAccessである。
普段、私はtsconfig.jsonの設定をあまり意識しておらず、noUncheckedIndexedAccessが何をするものなのかすら理解していなかった。
そのときは、たまたまnoUncheckedIndexedAccessがtrueになっており、型チェックでエラーが発生した。
noUncheckedIndexedAccessをtrueにすると、配列の要素へ添字を使ってアクセスした際に、値がundefinedである可能性を型として知らせてくれる。
const arr = ["ラーメン", "やきとん", "刺身"];
const val = arr[3]; // string | undefined
たとえば、次のような関数があるとする。
function getFood(index: number): string {
const arr = ["ラーメン", "やきとん", "刺身"];
const val = arr[index]; // string | undefined
return val;
}
noUncheckedIndexedAccessがtrueの場合、valはstring | undefinedと推論される。そのため、戻り値の型としてstringを指定しているこの関数では型エラーになる。
型エラーを解消するには、たとえば次のようにundefinedを処理する必要がある。
function getFood(index: number): string {
const arr = ["ラーメン", "やきとん", "刺身"];
const val = arr[index]; // string | undefined
if (val === undefined) {
throw new Error("0から2までの数字を指定してください");
}
return val;
}
JavaScriptでは、配列の範囲外の添字を指定するとundefinedが返ってくる。そのため、場合によってはバグの原因になり得る。
このことから、私はTypeScriptを使うのであれば、noUncheckedIndexedAccessをtrueにしてもよいのではないかと考えた。
AIと壁打ちをする中で、関連するIssueやPull Requestをいくつか提示してもらった。その中で紹介されたのが、TypeScriptの公式Wikiにある「TypeScript Design Goals」だった。
私は、そこに書かれている内容に興味を持った。
TypeScript Design Goals
TypeScriptの設計思想は、公式Wikiの「TypeScript Design Goals」にまとめられている。
私は英語が読めないため、翻訳こんにゃくを使いながら読んでいる。 このページには、TypeScriptのGoal、Goals、Non-goalsがそれぞれ記載されている。
ここからは、その中のいくつかを見ていく。
エラーになりそうなコードを実行前に見つける
Goalsの1つ目には、次のように書かれている。
Statically identify constructs that are likely to be errors.
これは、プログラムを実行する前に、バグにつながる可能性が高いコードをTypeScriptの型チェックによって見つけるという意味である。
たとえば、存在しないプロパティへのアクセスや、関数が期待する型とは異なる値の受け渡しなどを静的に検出する。
interface User {
name: string;
}
const user: User = {
name: "Taro",
};
console.log(user.age);
User型にはageというプロパティが存在しないため、TypeScriptはこのコードを実行する前に型エラーとして検出する。
また、TypeScriptは次のようなコードも検出できる。
function double(value: number) {
return value * 2;
}
double("10");
doubleはnumber型の値を受け取る関数だが、ここではstring型の値を渡しているため、型エラーになる。
このようにTypeScriptは、実行時に失敗する可能性が高いコードの構造を、コンパイル時の静的解析によって見つけることを目標としている。
一貫性があり、完全に消去可能な構造的型システムを使用する
Goalsの9つ目には、次のように書かれている。
Use a consistent, fully erasable, structural type system.
これは、TypeScriptの型情報を、基本的にはJavaScriptへの変換時にすべて取り除けるようにするという意味である。
たとえば、次のTypeScriptコードがあるとする。
const user: User = getUser();
これは、JavaScriptへ変換されると次のようになる。
const user = getUser();
型注釈である: Userが取り除かれているだけであり、型情報そのものはJavaScriptのコードには残らない。
TypeScriptは、実行時に型情報を残すのではなく、コンパイル時に値の構造をもとに型を確認し、その型情報をJavaScriptへの出力時に取り除く設計を目指している。
出力されるプログラムに実行時オーバーヘッドを課さない
Goalsの3つ目には、次のように書かれている。
Impose no runtime overhead on emitted programs.
これは、TypeScriptが型情報を使って静的なチェックを行う一方で、その型情報に基づく検査処理や変換処理を、実行時のJavaScriptへ自動的に追加しないという意味である。
たとえば、次のTypeScriptコードがあるとする。
const user: User = getUser();
このコードから、TypeScriptが次のようなJavaScriptを自動的に生成することはない。
if (!isUser(user)) {
throw new Error("その型ちゃうで");
}
このような実行時の型検査を行いたい場合は、開発者が明示的に実装する必要がある。
TypeScriptは型情報を使ってコンパイル時に静的な型チェックを行うが、その型情報に基づいた検査処理や変換処理を、実行時のJavaScriptへ自動的に追加することはしない。
健全な型システムは目指さない
Non-goalsの3つ目には、次のように書かれている。
Apply a sound or “provably correct” type system. Instead, strike a balance between correctness and productivity.
日本語に訳すと、次のようになる。
「証明可能な正しさ」を備えた健全な型システムを採用するのではなく、正しさと生産性のバランスを取る。
また、先ほど紹介したGoalsには、次の記述がある。
Statically identify constructs that are likely to be errors.
TypeScriptは、エラーになりそうなコードを静的に見つけることを目標の一つとしている。
一方で、型チェックを通過したプログラムの正しさを完全に保証するような、健全な型システムを作ることは目標としていない。
つまりTypeScriptは、型によってバグを見つけやすくすることを目指しているが、あらゆる危険なコードを禁止するところまでは目指していない。
正しさだけを追求するのではなく、JavaScriptの柔軟性や開発のしやすさとのバランスを取っている。
考察・まとめ
ほかにも多くの項目があるが、ここからは、私がTypeScript Design Goalsを読んで考えたことを記載する。
JavaScriptに近い記法
TypeScriptは、型情報に基づいた検査処理をJavaScriptのランタイムへ追加せず、基本的には型情報を取り除く形でJavaScriptへ変換される。そのため、変換後のコードも、人が直接書いたJavaScriptに近い形になるよう設計されている。実際にTypeScriptを書いていると、JavaScriptに型を付けた言語であるように感じる。冒頭で、TypeScriptはAltJSの一種であると記載した。AltJSには、ElmやPureScriptなど、ほかの言語も存在する。その中でTypeScriptが広く利用されている理由の一つには、JavaScriptに近い記法を採用することで、JavaScriptプログラマーが比較的参入しやすい環境を作れていることがあるのかもしれない。2025年には、TypeScriptがGitHub上で最も使われているプログラミング言語となった。
言語として健全な型システムを目指さない
TypeScriptは、言語として完全に健全な型システムを実現することを目標としていない。私は、健全性を言語側で厳しく追求すると、JavaScriptに近い記法や柔軟性から離れていく可能性があるためではないかと考えた。 この点については、賛否が分かれるだろう。 そもそも「言語として健全な型システムを提供するべきだ」と考える人は、TypeScriptをそれほど好まない可能性もある。そのような人は、言語自体がより厳密な型システムを持つ別のプログラミング言語を選ぶのかもしれない。
堅牢な型設計を自分たちで作っていく
TypeScriptが言語として完全に健全な型システムを提供していないからといって、堅牢なプログラムを作れないわけではない。
ZodやValibotといったバリデーションライブラリを利用したり、アプリケーション内で厳密な型設計を行ったりすることで、メンバーがミスをしにくく、バグが発生しにくい構造を作ることはできる。現在はAIによってコーディングのハードルが下がっている。一方で、存在しないはずの状態を型として定義していないか、想定していない値が関数へ渡る可能性がないかなどを、これまで以上に厳密に確認することが大切になる。TypeScriptがすべてを保証してくれるわけではないからこそ、どのような制約を設け、どのような状態を型として表現するのかを、自分たちで考える必要がある。
領域ごとに異なるTypeScriptを使うモチベーション
TypeScriptは、フロントエンド、バックエンド、インフラ、CLIなど、さまざまな領域で使用できる。 TypeScript Design Goalsを見る限り、TypeScriptを使う目的やモチベーション、型に対する考え方は、領域ごとに異なるのではないかと考えている。 たとえば、ブラウザ上で動くフロントエンドと、外部からの入力を受け取るバックエンドでは、型や実行時検証に求めるものも異なる可能性がある。ただし、私自身もまだ明確な答えを出せていない。
この点については、あらためて調査し、別の記事として書こうと思う。
感想
TypeScriptが、なぜ言語として完全に健全な型システムを目指していないのかという疑問が晴れたことはよかった。 このことを知ったことで、領域ごとにどのようなエラーハンドリングや設計を採用し、どのようなモチベーションでTypeScriptの型を利用すればよいのかが、さらに気になるようになった。 TypeScriptを使う目的によって、適切な型定義や設計も変わってくるだろう。 今後は、そのようなテーマを持って研究していこうと思う。
