はじめに:テストコードから生まれた疑問
TypeScriptで次のようなテストコードを書いているときに思いました。
describe("ショッピングリストのテスト", () => {
let shoppingList: ShoppingList
shoppingList = new ShoppingList()
})
「あれ、そういえばなんでクラスって型として使えるんだっけ?」と。
TypeScriptには型を定義する方法がいくつかありますよね:
-
interfaceでインターフェースを定義 -
typeで型エイリアスを定義 -
classでクラスを定義
でも、なぜ class で定義したものが型としても使えるのでしょうか?この記事では、その設計意図を簡単に整理します。
TypeScriptの二重性:型の世界と値の世界
TypeScriptには、2つの世界が存在します。
-
型の世界(Type Space): コンパイル時にのみ存在。
type、interfaceなど。JavaScript出力では消える。 -
値の世界(Value Space): 実行時に存在。
class、const、functionなど。JavaScript出力後も残る。
クラスの二重性
クラスは、同じ名前(例:ShoppingList)で「値(コンストラクタ/静的メンバー)」と「型(そのインスタンスの形)」を提供する代表例です。
コンパイルの前後で次のように変わります。
class ShoppingList { // 値でもあり型でもある
public list: string[];
constructor() {
this.list = [];
}
}
// ↓ JavaScriptに変換すると...
// 【値の世界】クラス定義は残る
class ShoppingList { // 値になる
constructor() {
this.list = [];
}
}
補遺:クラス以外の二重性の例
- 関数は「値としての実体」と「呼び出しシグネチャの型」を同名で持つ
- enum は「値としてのマップ」と「メンバーのユニオン型」を提供(const enum は値が消える)
- 名前空間は「値としてのオブジェクト」と「中に宣言された型」を併せ持ち、同名のクラスや関数とマージできる
よく、暗記用の赤いシートをかぶせると文字が見えなくなる教材がありますよね。TypeScriptもそれに例えることができるのではないかと思いました。
- 型は赤インク:コンパイル(赤いシート)をかぶせると見えなくなるので、JavaScriptの出力には現れない。
- 値は黒インク:赤いシートをかぶせても消えないので、実行時に残る。
- クラスは赤と黒の両方:同じ名前で「型(赤)」と「値(黒)」を提供し、型は消えるが、コンストラクタや静的メンバーといった値は出力に残る。
「値」と「型」を共有する、4つの視点
1:JavaScript互換を最優先にした設計
TypeScriptは既存のJavaScriptの上に型付けを重ねる思想です。JavaScriptでクラス(コンストラクタ関数)は実行時の値として new や instanceof に参加します。これに対応して同名の「型」を持たせることで、コードと型の対応がズレにくくなります。
2:同名で「値」と「型」を持つから、実行時の仕組みを型安全に使える
クラスはコンストラクタ・静的メンバーという「値」と、インスタンスの形という「型」を同じ名前で提供します。型はコンパイル時に消え、値は実行時に残るため、new や instanceof、静的メンバー参照などの挙動をそのまま使いながら、型注釈で整合性を保てます。
3:型だけ(interface/type)は実行時と直接は結び付かない
結果として、type や interface は値を生成しないため、instanceof、静的メンバー、初期化ロジックといった実行時の仕組みとは直接結び付きません。実行時の挙動と静的な保証を両立したい場面では、クラスの二重性が有効に働きます。
4:クラスだけが持つ名義的な安全性(private/protected)
TypeScriptは基本は構造的型ですが、クラスに private/protected が含まれる場合、互換性は「同一宣言起源」でのみ成立します。見た目が同じでも別クラスを誤代入しにくくなり、安全性が上がります。
まとめ:
- 目的: 「JavaScriptの実在」と「静的型」の同期。
- クラスの役割: 「値(コンストラクタ・実装)」と「型(インスタンスの形)」を同名で提供(宣言の二重性)。
-
効果: 重複定義を減らし、実装と型のズレを抑え、
instanceof・静的メンバー・継承などの実行時機構を支える。
TypeScriptでクラスを型として使えるのは、JavaScriptとの互換性を保ちながら型安全性を提供するという設計思想の表れですね。理解が進むと、ジェネリクスやユーティリティ型なども、より筋道立てて捉えられるのだろうと思います。型はコンパイル時だけ、「実行時は値だけが動く」という前提をしっかり整理して理解を深めたいと思います。
株式会社シンシア
株式会社シンシアでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
弊社には年間100人程度の実務未経験の方に応募いただき、技術面接を実施しております。
この記事が少しでも学びになったという方は、ぜひ wantedly のストーリーもご覧いただけるととても嬉しいです!