こんにちは。TypeScriptという言語の深淵へようこそ。
現場でコードを書いていると、「あれもこれも機能が必要だ」と型がどんどん巨大化し、気づけば迷宮のような継承関係に頭を抱える……そんな経験はありませんか?
TypeScriptの型システムは強力です。しかし、その強力さを「継承(`extends`)」という古いパラダイムだけで扱おうとすると、途端にコードは硬直化します。今回は、「継承ではなく合成(コンポジション)で型を作る」という、プロの現場で必須となる設計哲学を伝授します。
ここをクリアすれば、あなたの書く型は劇的に洗練されますよ。
—
1. なぜ「継承」に頼りすぎてはいけないのか
多くの開発者がやりがちなのが、これです。
interface Animal { name: string; }
interface Mammal extends Animal { fur: string; }
interface Dog extends Mammal { bark(): void; }
一見きれいですが、これには大きな罠があります。「犬(Dog)」という型を作るときに、「哺乳類(Mammal)」や「動物(Animal)」という歴史まで全て背負わなければなりません。もし「毛はあるけど動物ではない何か(例えば架空のAIロボット)」を作ろうとしたとき、この階層構造は崩壊します。
「継承は is-a 関係(〜は〜である)を強いるが、合成は has-a 関係(〜を持つ)を作る」
この違いを意識するだけで、あなたの設計は一気に柔軟になります。
—
2. コンポジション(合成)による型構築の極意
コンポジションの基本は、「小さなパーツを混ぜ合わせる」ことです。TypeScriptにおいて、これを実現する最強のツールが「インターセクション型(`&`)」です。
基本パーツを作る
まずは、機能単位で小さな型を定義します。
// 名前を持つという機能
interface Named {
name: string;
}
// ログ出力機能を持つ
interface Loggable {
log: () => void;
}
// ネットワーク通信機能を持つ
interface Networkable {
fetch: (url: string) => Promise
}
合成する(混ぜ合わせる)
これらを組み合わせて、必要な型を一瞬で作ります。
// 「名前を持ち」かつ「ログ出力ができる」型を合成
type User = Named & Loggable;
const admin: User = {
name: “Admin”,
log: () => console.log(“Logged in”)
};
継承と決定的に違うのは、「いつでも必要な機能だけをつまみ食いできる」という点です。階層に縛られない、非常に身軽な設計が可能になります。
—
3. インターフェースの「宣言結合」という裏技
インターフェースには、型エイリアスにはない強力な特性があります。それが「宣言結合(Declaration Merging)」です。同じ名前でインターフェースを複数回定義すると、勝手に合体してくれます。
interface Config {
url: string;
}
// 後から機能を追加する(ライブラリの拡張などでよく使います)
interface Config {
timeout: number;
}
// 結果: { url: string; timeout: number; } になる
const myConfig: Config = { url: “api.com”, timeout: 5000 };
これを使うと、コードの離れた場所からでも「この型にはこの機能が必要だ」と付け足すことができます。モジュール設計において、非常に強力な武器になりますね。
—
4. 陥りやすい罠:プリミティブ型の合成
初心者がよくやるエラーがこちらです。
type ID = string & number; // 存在しない型!
`string` であり、かつ `number` である値は、この世には存在しません(これを `never` 型と呼びます)。型を合成するときは、「オブジェクト同士を合成しているか?」を常に意識してください。
また、同じプロパティ名で異なる型を合成しようとすると……
interface A { id: string; }
interface B { id: number; }
type C = A & B;
// C.id は string & number になるため、結果的に never になります。
合成するときは、プロパティの衝突にも気を配るのがプロの作法です。
—
5. まとめ:型は「積み木」である
継承という「一本の木」を育てるのではなく、コンポジションという「積み木」で形を作る。これがTypeScriptにおける現代的な型設計の正解です。
- 継承(`extends`)は、明確な階層構造がある場合のみ限定的に使う。
- 合成(`&`)をメインにして、機能単位で型を小さく保つ。
- インターフェースは、宣言結合を活用して拡張性を確保する。
型は単なる制約ではありません。あなたのコードが「どうあるべきか」を記述する設計図そのものです。
まずは今日、既存の巨大なインターフェースを分解するところから始めてみてください。「あれ、これとこれは別に分離できるな」と気づいたとき、あなたはもうTypeScriptの深淵に片足を踏み入れています。
これからも、一緒に賢く、美しくコードを書いていきましょう。応援していますよ。