おい、調子はどうだい?
最近、君が書いているコードをレビューしていて思ったんだ。「お、結構型安全を意識して書けるようになってきたな」ってね。ただ、大規模なアプリケーションを組んでいくと、どうしても似たようなプロパティを持つオブジェクトの型定義にぶつかるだろ?
「あれ、さっき別のコンポーネントでもこのプロパティ群書かなかったっけ?」ってコピペの衝動に駆られた瞬間、それは型設計を見直すサインだ。
今回は、そんな君にTypeScriptの真骨頂の一つである `extends` を使ったインターフェースの継承について、実務の現場で泥臭く生き抜くためのノウハウを授けよう。公式ドキュメントの斜め上を行く、現場のリアルなプラクティスを叩き込んでやるから、心して聞いてくれ。
—
なぜ `extends` なのか?DRY原則を型定義にも持ち込め
フロントエンド開発、特にReactやVueあたりで複雑なUIコンポーネントを組み立てていると、APIから返ってくるデータ構造と、UIのコンポーネントが受け取るPropsの構造が微妙に被ることがよくあるよな。
ここで、何も考えずにすべてのプロパティをベタ書きしているコードを見かけると、おじさんは涙が出そうになる。
例えば、ユーザー情報の基本型があって、それを拡張して「詳細画面用」「編集画面用」の型を作りたいとき、どうする? コピペか? 違うよな。
TypeScriptの `extends` キーワードを使えば、既存のインターフェースが持つプロパティをごっそり継承しつつ、差分だけを追加・上書きすることができる。これはオブジェクト指向のクラス継承とは違って、純粋な「型の集合論(部分型関係)」の話なんだ。
現場で即決すべき基本パターン
まずは、百聞は一見にしかずだ。実務でそのまま使える綺麗なサンプルコードを見せよう。
// ==========================================
// 1. 基本の型定義(ベースとなるユーザー情報)
// ==========================================
interface BaseUser {
id: string;
name: string;
email: string;
createdAt: string;
}
// ==========================================
// 2. extendsを使ったインターフェースの継承
// ==========================================
// 2-1. プロパティの追加(最も一般的な使い方)
interface UserProfile extends BaseUser {
bio: string; // 自己紹介文を追加
avatarUrl: string; // プロフィール画像のURLを追加
}
// 2-2. 複数継承(複数のインターフェースを組み合わせる)
interface Timestamp {
updatedAt: string;
version: number;
}
interface AdminUser extends BaseUser, Timestamp {
permissions: (‘read’ | ‘write’ | ‘delete’)[];
securityLevel: number;
}
どうだい?すっきりしているだろう。`AdminUser` なんて、`BaseUser` と `Timestamp` の両方を `implements` ならぬ `extends` でスマートに合体させている。これにより、ベース側の仕様変更(例えば `email` がオプショナルになったり)があった場合も、一箇所を直すだけですべての子孫型に伝播してくれる。これが保守性というやつだ。
—
ブラウザやコンパイラ(tsc)の裏側で何が起きているのか?
さて、ここで少し視点を変えて、TypeScriptのコンパイラ(`tsc`)がこの `extends` をどう処理しているか、その裏側の話をしよう。
TypeScriptは、最終的にブラウザが理解できる「ただのJavaScript」にコンパイルされる。ここで重要なのは、JavaScriptの実行時にはインターフェースも `extends` も跡形もなく消え去っているという事実だ。
コンパイラは、コードをパースして抽象構文木(AST)を作り、型チェック(Type Check)のフェーズでこの `extends` の関係性を検証する。
コンパイラは内部で「`UserProfile` は `BaseUser` のすべてのプロパティを包含しているか?」という部分型(Subtyping)の判定を行っているんだ。
- `UserProfile` のインスタンスやオブジェクトは、TypeScriptの型システム上において `BaseUser` が要求されるすべての場所(関数引数など)に代入可能(リスコフの置換原則の型バージョン)になる。
- しかし、JavaScriptのランタイムにはクラスの `extends` のようなプロトタイプチェーンの変更は一切発生しない。あくまで「静的な静的解析のルール」として存在している。
この割り切りがあるからこそ、TypeScriptは実行時コストをゼロに抑えながら、これほど強力な型安全性を開発者に提供できるんだ。素晴らしい仕組みだと思わないか?
—
現場のシニアが教える「やってはいけない」アンチパターン
さて、ここからが一番大事な話だ。機能の仕組みを知るだけなら誰でもできるが、現場で事故らないための「知見」を身につけるのがプロの条件だ。
`extends` を使うときに、中級者がやりがちな「やってはいけない罠」をいくつか共有しておこう。
1. プロパティの「型上書き(オーバーライド)」の罠
子インターフェースで、親インターフェースにある既存のプロパティの型を無理やり変えようとしていないか?
interface Parent {
id: string | number;
}
// やりがちな悪い例:親の型と互換性がない型に変えてしまう
interface Child extends Parent {
id: boolean; // ❌ これをやるとコンパイルエラーになるか、思わぬバグの元に
}
TypeScriptは優秀だから、親の型と子で上書きした型に互換性がない場合、容赦なくエラーを出してくれる。もしどうしても型を絞り込みたい(Narrowing)なら、継承ですべてを解決しようとせず、ユニオン型やユーティリティ型(Omitなど)の併用を検討すべきだ。
2. 深すぎる継承階層(アンチ・オブジェクト指向かぶれ)
JavaやC#出身のバックエンドエンジニア上がりの人がやりがちなのが、`BaseEntity` から `UserEntity` を継承し、さらにそれを継承して……と、3階層も4階層も `extends` を重ねる地獄の構造だ。
フロントエンドのUIやAPIの型定義でこれをやると、エディタのホバー(IntelliSense)で型を見たときに、どこのプロパティがどこから来ているのか全く追えなくなる。いわゆる「メンテ不能なスパゲッティ型」の完成だ。
【シニアからのベストプラクティス】
フロントエンドの型定義における `extends` は、原則として「1階層(せいぜい2階層まで)」にとどめろ。合成(Composition)の思想、つまり複数の小さなインターフェースを交差型(Intersection Types: `&`)や多重継承で組み合わせるアプローチの方が、変更に対して圧倒的に強い。
—
実践:APIレスポンスの共通化に `extends` を活かす
最後に、実務で明日から使える実践的なコードを紹介して締めくくろう。
Webフロントエンドで一番頭を悩ませるのが、APIの共通レスポンス構造と、個別のデータ型のマッピングだ。
// ==========================================
// 実務で使える:APIレスポンスの型設計
// ==========================================
// 1. すべてのAPIレスポンスに共通するメタデータ
interface ApiResponseMeta {
status: number;
message: string;
timestamp: string;
}
// 2. ページネーションが必要なAPI用の共通型
interface PaginatedMeta extends ApiResponseMeta {
totalCount: number;
currentPage: number;
totalPages: number;
}
// 3. ユーザーデータの基本型
interface User {
id: string;
name: string;
}
// 4. 「ユーザー一覧取得API」のレスポンス型をスマートに定義
interface GetUsersResponse extends PaginatedMeta {
data: User[];
}
// 5. 「単一ユーザー取得API」のレスポンス型を定義
interface GetUserDetailResponse extends ApiResponseMeta {
data: User & {
profile: {
bio: string;
website: string;
};
};
}
この設計を見てくれ。メタ情報の共通化をしつつ、ページネーションの有無や、詳細データのインライン拡張(交差型の活用)をキレイに住み分けできている。これなら、バックエンドの仕様変更にも怖じ気づくことなく、最小限の修正でフロント側の型を追従させることができるはずだ。
—
おわりに
インターフェースの継承(`extends`)は、ただコードを短くするための手品じゃない。「チーム全体でドメインの構造を共通認識化し、変更に強いコードベースを維持するためのインフラ」なんだ。
明日からコードを書くときは、「この `extends` は本当に保守性を高めているか? それとも単に階層を深くして読みにくくしているだけじゃないか?」と、一歩立ち止まって自問自答してみてほしい。
君ならできる。それじゃあ、今日も最高のコードを書きにいこうぜ!

コメント