ゴール:「1つのクラスに“複数の役割”を約束させる感覚をつかむ」
多重 interface 実装は一言でいうと、
「このクラスは A という顔も B という顔も、両方ちゃんと持ちますよ」
と約束させる仕組みです。
implements の右側に interface をカンマ区切りで並べるだけですが、
「役割を分けて考える」という設計の感覚が身につくと、一気に使いどころが見えてきます。
多重interface実装の基本構文
まずは単一 interface 実装をおさらい
単体の interface を implements する形はこうでした。
interface HasId {
id: number;
}
interface CanSave {
save(): void;
}
class User implements HasId, CanSave {
constructor(
public id: number,
public name: string
) {}
save(): void {
console.log(`ユーザー ${this.name} を保存しました`);
}
}
TypeScriptここで class User implements HasId, CanSave と書くことで、
id: numberを持っていることsave(): voidを持っていること
この両方を「必ず守ります」と宣言しています。
ポイントは、
「1つのクラスが“複数の interface の契約”を同時に守る」
という状態を作れることです。
カンマ区切りでいくつでも並べられる
構文としてはとてもシンプルで、implements A, B, C のようにカンマで並べるだけです。
interface HasId {
id: number;
}
interface HasName {
name: string;
}
interface CanGreet {
greet(): void;
}
class Person implements HasId, HasName, CanGreet {
constructor(
public id: number,
public name: string
) {}
greet(): void {
console.log(`こんにちは、${this.name}です`);
}
}
TypeScriptこの Person は、
HasIdとしてもHasNameとしてもCanGreetとしても
扱えるクラスになっています。
「多重interface実装」が何をうれしくするのか
クラスに「複数の役割」をはっきりラベル付けできる
例えば、あるクラスが「ID を持つ」「表示できる」「保存できる」という
3つの役割を持っているとします。
interface HasId {
id: number;
}
interface Displayable {
display(): void;
}
interface Savable {
save(): void;
}
class Product implements HasId, Displayable, Savable {
constructor(
public id: number,
public name: string,
public price: number
) {}
display(): void {
console.log(`${this.name} (${this.price}円)`);
}
save(): void {
console.log(`商品 ${this.name} を保存しました`);
}
}
TypeScriptここでの設計のポイントは、
Productというクラスに「HasId」「Displayable」「Savable」という
3つの“役割ラベル”が付いている- それぞれの interface は「その役割に必要なメンバー」だけを定義している
- クラスはそれらを全部まとめて実装している
ということです。
「このクラスは何ができるのか?」を、
interface の一覧として明示できる
のが、多重 interface 実装の気持ちよさです。
呼び出し側は「必要な顔だけ」を見ることができる
同じ Product でも、
使う側によって「見たい顔」が違います。
function printId(target: HasId) {
console.log("ID:", target.id);
}
function show(target: Displayable) {
target.display();
}
function persist(target: Savable) {
target.save();
}
const p = new Product(1, "ノートPC", 150000);
printId(p); // HasId として使う
show(p); // Displayable として使う
persist(p); // Savable として使う
TypeScriptここでのポイントは、
Productは 3つの interface を全部実装している- でも、関数側は「自分が必要な役割だけ」を引数の型として要求している
ということです。
「クラスは多機能でも、呼び出し側は必要な機能だけを前提にできる」
これが、多重 interface 実装の大きなメリットです。
型の観点から見る多重interface実装
「A でもあり B でもある」という型になる
implements A, B と書いたクラスは、
型として「A でもあり B でもあるもの」として扱えます。
interface A {
a(): void;
}
interface B {
b(): void;
}
class X implements A, B {
a(): void {
console.log("A の機能");
}
b(): void {
console.log("B の機能");
}
}
const x = new X();
const asA: A = x;
const asB: B = x;
asA.a(); // OK
// asA.b(); // A 型としては b は見えない
asB.b(); // OK
TypeScript同じ x でも、
A型として見ると「a()だけ持っているもの」B型として見ると「b()だけ持っているもの」
として扱えます。
「1つの実体に、複数の“見え方(型)”を与える」
というのが、多重 interface 実装の本質です。
実装側は「全部の契約を守る」必要がある
もちろん、クラス側は
実装をサボることはできません。
interface A {
a(): void;
}
interface B {
b(): void;
}
class Bad implements A, B {
a(): void {
console.log("A だけ実装");
}
// b() を実装していないのでコンパイルエラー
}
TypeScript多重 interface 実装は、
「このクラスは、これらすべての契約を守ります」と
自分でハードルを上げる行為
でもあります。
だからこそ、「このクラスは本当にその役割を全部持つべきか?」を
意識して設計することが大事です。
多重interface実装と継承の組み合わせ
クラス継承+複数 interface 実装もできる
extends と implements は同時に使えます。
interface CanFly {
fly(): void;
}
interface CanSwim {
swim(): void;
}
class Animal {
constructor(public name: string) {}
}
class Duck extends Animal implements CanFly, CanSwim {
fly(): void {
console.log(`${this.name} が飛んだ`);
}
swim(): void {
console.log(`${this.name} が泳いだ`);
}
}
TypeScriptここでは、
- 継承(
extends Animal):共通の実装・プロパティ(name)を引き継ぐ - 多重 interface 実装(
implements CanFly, CanSwim):
「飛べる」「泳げる」という2つの役割を約束する
という役割分担になっています。
「共通の中身は継承で共有し、
“できることのラベル”は interface で付ける」
という設計は、かなり実務でよく使われます。
多重interface実装を設計するときの考え方
「役割ごとに interface を分ける」意識を持つ
多重 interface 実装をうまく使うには、
interface を「役割ごとに小さく分ける」意識が大事です。
例えば、こんな巨大 interface は扱いづらいです。
interface Monster {
id: number;
name: string;
hp: number;
attack(): void;
defend(): void;
talk(): void;
// まだまだ増える…
}
TypeScriptこれを役割ごとに分けてみます。
interface HasStatus {
hp: number;
}
interface CanAttack {
attack(): void;
}
interface CanDefend {
defend(): void;
}
interface CanTalk {
talk(): void;
}
TypeScriptそして、クラスごとに必要なものだけ implements します。
class Slime implements HasStatus, CanAttack {
hp = 10;
attack(): void {
console.log("スライムの攻撃!");
}
}
class NPC implements CanTalk {
talk(): void {
console.log("こんにちは、旅人さん");
}
}
TypeScriptこうすると、
- 「このクラスは何ができるのか」が
implementsの一覧で一目で分かる - 関数側も「必要な役割だけ」を引数の型として要求できる
という、きれいな設計になります。
まとめ:多重interface実装を自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
多重 interface 実装は、
implements A, B, Cのように書いて、
1つのクラスに複数の「契約(役割)」を守らせる仕組み- クラスは、そのすべての interface のメンバーを正しい型で実装しなければならない
- 呼び出し側は「必要な interface 型だけ」を前提にコードを書ける
- その結果、同じクラスを「いろいろな顔(型)」で扱えるようになる
今あなたのコードの中で、
「1つのクラスがいろんなことをしているのに、
それを1つの型としてしか扱っていない場所」
があれば、
- その“役割”を小さな interface に分解して
- クラスに多重
implementsさせて - 関数側は「必要な interface 型だけ」を受け取る
という形にできないか、1カ所だけでいいので考えてみてください。
そこから、
“なんでも屋クラス”が、“役割ごとにラベル付けされたクラス”に
少しずつ変わっていきます。
