こんにちは!TypeScriptの型システムの世界へようこそ。
日々、フロントエンドからバックエンドまでコードを書いていると、「あれ、この型、さっきも別の場所で書いたぞ…?」と手が止まる瞬間はありませんか?
他のオブジェクト指向言語(JavaやC#など)を経験した方だと、ついつい `extends` というキーワードを重ねて、立派な「継承のピラミッド」を作りたくなってしまうものですよね。
でも、ちょっと待ってください。TypeScriptの真骨頂は、がっちり組まれた継承の塔ではなく、「パーツをカチャカチャと組み合わせる(コンポジション)」ような、しなやかで疎結合な型設計にあるんです。
今回は、インターフェース(Interface)の拡張と合成を使いこなして、変更に強く、見通しの良い型設計を手に入れる方法を一緒に紐解いていきましょう。ここをクリアすれば、あなたのTypeScriptのコードは一気に洗練されますよ!
—
1. なぜ「継承のピラミッド」より「パーツの組み合わせ」なのか?
オブジェクト指向のクラス継承では、親から子へ、孫へと機能が引き継がれていきます。
[User] (親)
└─ [AdminUser] (子)
└─ [SuperAdminUser] (孫)
一見きれいに見えますが、これには「仕様変更のたびにピラミッド全体が揺らぐ」という大きな弱点があります。例えば、「すべてのユーザーにログイン履歴を持たせたいけれど、特権管理者には不要」なんて要件が出た途端、継承のツリー構造が破綻してしまうんですよね。
そこで登場するのが、TypeScriptのコンポジション(合成)指向です。
小さくて独立したインターフェースをいくつも用意し、必要なものを必要なだけ組み合わせていくアプローチをとります。
イメージとしては、LEGOブロックのパーツ箱です。「名前を持つパーツ」「連絡先を持つパーツ」「権限を持つパーツ」を組み合わせて、キャラクターを作っていく感覚ですね。
—
2. Interfaceの「拡張(extends)」の基本とスマートな使い方
まずは、TypeScriptの基本である `extends` を使ったインターフェースの拡張を見てみましょう。
「すでにある型をベースにして、新しいプロパティを追加したい」というときに使います。
// 1. 基本となる「名前」のパーツ
interface Named {
name: string;
}
// 2. 基本となる「連絡先」のパーツ
interface Contactable {
email: string;
phone?: string; // あってもなくても良いプロパティ(オプショナル)
}
// 3. これらを「拡張」して、新しい「User」型を作る
interface User extends Named, Contactable {
id: string;
createdAt: Date;
}
// 使い方
const author: User = {
id: “usr_001”,
name: “Yamada Taro”,
email: “yamada@example.com”,
// phone はオプショナルなので省略可能!
createdAt: new Date(),
};
ここでのポイントは、`User` インターフェースが `Named` と `Contactable` の両方を `extends`(継承・拡張)している点です。
「,(カンマ)」で区切ることで、複数のインターフェースを同時に拡張(多重継承)できるのがTypeScriptの強力なところです。これにより、小さな責務に分割されたパーツを綺麗にまとめることができます。
—
3. 「交差型(Intersection Types / `&`)」による合成のアプローチ
さて、ここからが少し応用的な、しかし実務でめちゃくちゃよく使うテクニックです。
インターフェースは `extends` キーワードだけでなく、交差型(`&`)を使うことでも合成できます。
「あれ、`extends` と何が違うの?」と思いますよね。
`extends` は「宣言時」に型同士の関係性を固定しますが、交差型(`&`)は「既存の型と型を合体させて新しい型を作る(演算する)」という感覚に近いです。
interface WithId {
id: string;
}
interface WithTimestamps {
createdAt: Date;
updatedAt: Date;
}
// 交差型(&)を使って、既存のインターフェース同士を合体させる
type ApiResource
// 具体的なデータ型の定義
interface Post {
title: string;
body: string;
}
// Post に ID とタイムスタンプが自動的に合体する!
type PostResource = ApiResource
const myPost: PostResource = {
id: “post_999”,
title: “TypeScriptの型設計について”,
body: “インターフェースの合成は最高です…”,
createdAt: new Date(),
updatedAt: new Date(),
};
この「交差型による合成」は、ジェネリクス(総称型)と組み合わせたユーティリティ型を作るときに真価を発揮します。わざわざ新しいインターフェース宣言を書かなくても、その場でパパッと型を組み合わせられるのが大きなメリットです。
—
4. 陥りがちな文法エラーと、その回避の処方箋
インターフェースの拡張や合成を使っていると、初学者のうちによくハマる「罠」があります。
代表的なエラーとその対処法を知っておきましょう。
トラブル事例:プロパティの型が衝突(コンフリクト)する!
例えば、あるパーツでは `id` が `string` 型で、別のパーツでは `id` が `number` 型だった場合、どうなるでしょうか?
interface Foo {
id: string;
}
interface Bar {
id: number; // 型が違う!
}
// 拡張しようとすると…
// interface Baz extends Foo, Bar {
// // ❌ エラー!
// // Interface ‘Baz’ cannot simultaneously extend types ‘Foo’ and ‘Bar’.
// // Named property ‘id’ of types ‘Foo’ and ‘Bar’ are not identical.
// }
【処方箋】
TypeScriptのコンパイラは非常に優秀なので、互換性のない型同士が同じ名前で衝突したとき、適当に丸め込まずにちゃんとエラーを出して教えてくれます。
もし交差型(`&`)でこれを行おうとすると、`id` の型は `string & number` という「絶対に存在しない値(Never型)」になり、思わぬバグの原因になります。
型を拡張・合成する際は、「同じ名前のプロパティで型を矛盾させない」のが大原則です。もし型を上書き(オーバーライド)したい場合は、ユーティリティ型(`Omit` など)を駆使して、不要なプロパティを一度削ぎ落としてから結合するテクニックを使います。
// 例:Fooのid(string)を捨てて、number型のidを持つBarを合成する
type SafeBaz = Omit
—
5. まとめ:小さく作って、自由に組み合わせよう
今回は、Interfaceの拡張とコンポジション指向の型設計について解説しました。
- 継承のピラミッドを作らない:大きな共通親クラス(親インターフェース)を無理に作らず、小さなパーツに分割する。
- `extends` でスマートに拡張:複数のパーツを組み合わせて、意味のあるまとまりを作る。
- 交差型(`&`)で柔軟に合成:ジェネリクスなどと組み合わせて、その場で動的に型を組み立てる。
TypeScriptの型は、単なる「エラーを防ぐための網」ではありません。「自分やチームの未来のコードを守るためのドキュメント」であり、設計図そのものです。
「小さく作って、自由に組み合わせる」。このコンポジションの思想をモノにすれば、どんなに複雑な大規模アプリケーションの型設計でも、軽やかに楽しんで書けるようになりますよ。
それでは、また次の記事でお会いしましょう!バッチリマスターしていきましょうね!