こんにちは!TypeScriptの型システムの奥深い世界へようこそ。
日々、フロントエンドからバックエンドまでバリバリとコードを書いていると、「あれ、この型定義、`interface`の継承(`extends`)と、`type`の交差型(`&`)、どっちを使えばいいんだっけ?」と手が止まる瞬間ってありませんか?
どちらも「既存の型に新しいプロパティを追加して拡張する」という一見同じゴールにたどり着くため、何気なく選んでしまいがちです。しかし、コンパイラの内部挙動、エディタ(IDE)の補完速度、そして大規模開発におけるエラーメッセージの読みやすさにおいて、この2つには決定的な違いが存在します。
ここをクリアすれば、あなたのTypeScriptのコードは一段と洗練され、コンパイルも爆速になりますよ。さあ、一緒にその核心に迫っていきましょう!
—
1. 基礎知識:それぞれの構文と「型パズル」の正体
まずは、それぞれの基本的な使い方をおさらいしておきましょう。
`interface` の継承 (`extends`)
オブジェクト指向言語に慣れ親しんだ方にはお馴染みの構文ですね。「Aという基本の形があって、それを拡張してBを作る」という明確な親子関係を宣言します。
// 基本となるユーザーのインターフェース
interface User {
id: string;
name: string;
}
// Userを継承して、管理者の権限を追加
interface AdminUser extends User {
role: ‘admin’ | ‘superadmin’;
permissions: string[];
}
`type` の交差型 (`&`)
こちらは数学の集合論に近いアプローチです。「Aの性質 かつ(&) Bの性質を両方満たす新しい型を作る」という合成の概念になります。
// 基本となるユーザーの型
type UserType = {
id: string;
name: string;
};
// 交差型で管理者としての性質をマージする
type AdminUserType = UserType & {
role: ‘admin’ | ‘superadmin’;
permissions: string[];
};
「おや、書き方が違うだけで、結果は全く同じじゃないか」と思いましたよね?
実は、単一のオブジェクトを拡張する分には、出力される型もほぼ同じになります。では、何が違うのでしょうか? その秘密は、TypeScriptの心臓部である型チェッカーの計算ロジックに隠されています。
—
2. コンパイルの舞台裏:なぜ `interface` が速いのか?
ここからが少しディープで面白いところです。TypeScriptのコンパイラ(Tsc)が、これら2つをどのように評価(Evaluation)しているのかを覗いてみましょう。
`interface` は「キャッシュされた設計図」
`interface` は、定義された瞬間にその構造が名前付きでキャッシュされます。継承(`extends`)を使うと、コンパイラは「あ、この親の構造ね、そこにこれを足せばいいんだな」と、階層的なアトミック(不可分)な構造としてメモリ上に保持します。
そのため、エディタでプロパティの補完(IntelliSense)を呼び出したときも、コンパイラはスムーズにその構造を引くことができます。
交差型 (`&`) は「その場で計算されるパズル」
一方、交差型(`&`)は、「遅延評価される計算式」です。
例えば、以下のように何個もの型を `&` で繋ぎまくった巨大な型を作ったとします。
type DeepComplexType = TypeA & TypeB & TypeC & TypeD & TypeE;
TypeScriptの型チェッカーは、この型を使うコードに遭遇するたびに、「すべてのプロパティが衝突していないか」「同じプロパティがあった場合に型が矛盾(never)していないか」をその場でシミュレーション(計算)します。
これが大規模プロジェクトで何千回も行われるとどうなるでしょうか?
そう、コンパイル時間が目に見えて遅くなり、VSCodeなどのエディタでホバーしたときの型ヒントの表示がもたつく原因(パフォーマンスのボトルネック)になるのです。
—
3. エラーメッセージの優劣:開発体験(DX)の決定的な違い
コードを書いている最中に、万が一型を間違えたときのエラーメッセージ。ここにも大きな違いが出ます。
例えば、必須のプロパティを渡し忘れたとしましょう。
`interface` 継承の場合
const admin: AdminUser = {
id: “1”,
name: “Taro”
// role と permissions をわざと書き忘れています
};
【コンパイラーのメッセージ例】
> プロパティ ‘role’ は型 ‘AdminUser’ に必須ですが、型 ‘{ id: string; name: string; }’ 内には見つかりません。
非常にシンプルで、「あ、AdminUserに足りないんだな」と直感的に分かりますよね。
複雑な交差型 (`&`) の場合
これが複雑な交差型(特にジェネリクスやユーティリティ型が絡み合ったもの)になると、コンパイラは次のような難解なエラーを吐き出すことがあります。
> 型 ‘{ id: string; name: string; }’ を型 ‘TypeA & TypeB & TypeC & TypeD & TypeE’ に割り当てることはできません。
> プロパティ ‘permissions’ は次の型にはありませんが、型 ‘TypeE’ では必須です…(以下略)
型パズルの途中経過がそのままエラーメッセージに出てきてしまうため、初学者のうちは「どこを直せばいいの!?」とパニックになりがちです。
—
4. 陥りやすい罠:プリミティブ型の交差
ここで、初心者が必ずと言っていいほどハマる「落とし穴」をご紹介します。
「すべての型は交差型で書けるんでしょ?」と思って、以下のようなコードを書いたとします。
// 文字列型に、さらに絞り込んだ型を交差させようとする
type SpecialString = string & { readonly brand: unique symbol };
これは、ブランド型(Branded Types)と呼ばれる高度なテクニックで、プリミティブ型に対して有効です。
しかし、もしプロパティの型が衝突したらどうなるでしょう?
interface Box {
value: string;
}
interface NumericBox {
value: number; // string と number で衝突!
}
// interface ならどうなる?
// ❌ エラー:インターフェースの継承でプロパティの型を矛盾させるとコンパイルエラーになる
interface BadBox extends NumericBox, Box {}
// 交差型ならどうなる?
type MergedBox = Box & NumericBox;
// 🟢 ここで MergedBox[‘value’] の型はどうなると思いますか?
// 答えは string & number = “never” です!
交差型の場合、プロパティの型同士が矛盾すると、静かに `never`(存在し得ない型)に化けます。これにより、「コードは一見エラーなくコンパイル通るのに、実際に値を入れると絶対に型エラーになる謎のバグ」が生まれる原因になります。コンパイラが親切に怒ってくれないケースがあるため、交差型は諸刃の剣なのです。
—
5. 現場で役立つベストプラクティス(まとめ)
最後に、「じゃあ結局、どう使い分ければいいの?」という疑問に、チーフアーキテクトとしてズバッと答えましょう。
1. 基本は `interface` を使おう
- オブジェクトの構造を定義し、それを拡張していく通常のアプリケーション開発(ReactのProps定義やAPIのレスポンス型など)では、原則として `interface` と `extends` を選んでください。
- コンパイルが高速になり、エラーメッセージも人間にとって読みやすくなります。
2. `type` と交差型 (`&`) は「合成・ユーティリティ」のときだけ使おう
- 既存の複数の型を合体させるユーティリティ型を作る場合や、ユニオン型(`A | B`)やプリミティブ型、タプル型を扱うときは `type` の独壇場です。
- 例:`type ReadonlyUser = Readonly
;` や、関数型を組み合わせる場合など。
—
いかがでしたでしょうか?
「なんとなく動く」から一歩進んで、「コンパイラがどう解釈しているか」を意識できるようになると、TypeScriptを書く手が何倍も楽しく、そして速くなりますよ。
ここをクリアしたあなたなら、もう中級者の扉は完全に開いています。明日からのコードに、ぜひ活かしてみてくださいね!