TypeScript でクラスを設計するとき、implements というキーワードを使うと「このクラスは、この interface の形をきちんと満たしている」ことをコンパイラに約束させられます。約束を破って必要なメソッドを書き忘れると、その場でコンパイルエラーになるため、実装漏れを早い段階で防げます。この記事では、implements の基本的な書き方から、複数の interface を同時に実装する方法、クラス継承(extends)との違いと併用、そして「implements しても構造的型付けのままなので他の型と互換になる」といった、初心者がつまずきやすいポイントまで、動くコードとあわせて解説します。
目次
implements は「この形を満たす」という約束
implements は、クラス宣言に class クラス名 implements インターフェース名 の形で書きます。これを付けると、そのクラスは指定した interface が要求するプロパティとメソッドをすべて備えていなければならなくなり、TypeScript がコンパイル時にチェックしてくれます。まずは基本の例を見てみましょう。
// 「名前を持ち、鳴くことができる」という約束を interface で定義
interface Animal {
name: string;
speak(): string;
}
// implements で Animal の形を満たすことを宣言する
class Dog implements Animal {
constructor(public name: string) {}
speak(): string {
return `${this.name}はワンと鳴きます`;
}
}
const dog = new Dog('ポチ');
console.log(dog.speak()); // ポチはワンと鳴きます
Dog は Animal が要求する name プロパティと speak() メソッドの両方を持っているため、この宣言は問題なく通ります。implements Animal と書いておくことで、「このクラスは Animal として扱える形になっている」ことがコード上でひと目で分かり、なおかつコンパイラがそれを保証してくれるわけです。
なぜ implements を使うのか
implements を付けなくても、必要なプロパティとメソッドさえ書けばクラスは動きます。それでも implements を使う理由は、「このクラスがどんな役割を担うべきか」を宣言として明示し、その約束をコンパイラに守らせられる点にあります。たとえばチームで開発していて、決められた形(インターフェース)に沿ったクラスを複数用意したい場合、implements があれば形が崩れた瞬間にエラーで気づけます。
具体的なメリットを整理すると次のようになります。
| 観点 | implements を使うと得られること |
|---|---|
| 実装漏れの検出 | 必要なメソッドやプロパティを書き忘れると、コンパイル時にエラーになる |
| 意図の明示 | 「このクラスは○○という役割を果たす」ことがコードから読み取れる |
| 差し替えのしやすさ | 同じ interface を実装したクラスどうしを、同じ型として扱って入れ替えられる |
メソッドを実装し忘れるとコンパイルエラーになる
implements の一番の役割は、実装漏れをコンパイル時に知らせてくれることです。interface が要求するメンバーのうち1つでも欠けていると、クラス宣言そのものがエラーになります。
interface Animal {
name: string;
speak(): string;
}
// エラー: クラス 'Cat' はインターフェイス 'Animal' を正しく実装していません。
// プロパティ 'speak' がありません。
class Cat implements Animal {
constructor(public name: string) {}
// speak() を書き忘れている
}
Cat は name は持っていますが、speak() メソッドがないため「インターフェイスを正しく実装していません」というエラーになります。implements を付けていなければ、このクラスは(Animal として使おうとする箇所まで)エラーにならず、問題の発見が遅れてしまいます。implements は、こうした実装漏れをクラスを定義したその場で教えてくれる安全装置なのです。
複数の interface をまとめて実装する
1つのクラスは、複数の interface を同時に implements できます。class C implements A, B のようにカンマ区切りで並べると、A と B の両方の形を満たす必要が生じます。役割を小さな interface に分けておき、必要なものを組み合わせて実装する、という設計に向いています。
// 「泳げる」役割
interface Swimmer {
swim(): string;
}
// 「歩ける」役割
interface Walker {
walk(): string;
}
// 両方の役割を1つのクラスで実装する
class Duck implements Swimmer, Walker {
swim(): string {
return 'スイスイ泳ぐ';
}
walk(): string {
return 'ヨチヨチ歩く';
}
}
const duck = new Duck();
console.log(duck.swim(), duck.walk()); // スイスイ泳ぐ ヨチヨチ歩く
Duck は Swimmer と Walker の両方を実装しているので、swim() と walk() のどちらかでも書き忘れればエラーになります。このように役割ごとに interface を分けておくと、「泳げるものだけを受け取る関数」「歩けるものだけを受け取る関数」といった形で、必要な能力だけを型として要求できるようになります。
extends(クラス継承)との違いと併用
クラスには extends というキーワードもあり、implements としばしば混同されます。両者は目的がまったく異なります。extends は親クラスの実装(プロパティやメソッドの中身)を受け継ぐためのもので、implements は形を満たすことを約束するだけで、中身は受け継がないという違いがあります。
| キーワード | 役割 | 実装(中身)の継承 |
|---|---|---|
extends | 親クラスを継承する。指定できるのは1つのクラスだけ | 受け継ぐ |
implements | interface の形を満たすことを約束する。複数指定できる | 受け継がない(自分で書く) |
この2つは同時に使えます。書く順序は決まっていて、extends を先に、implements を後に書きます。親クラスから共通の実装を受け継ぎつつ、特定の interface の形も満たしていることをコンパイラに保証させる、という組み合わせです。
interface Loggable {
log(): void;
}
// 共通の実装を持つ親クラス
class Entity {
constructor(public id: number) {}
describe(): string {
return `Entity #${this.id}`;
}
}
// extends で Entity を継承しつつ、implements で Loggable の形も満たす
class User extends Entity implements Loggable {
constructor(id: number, public name: string) {
super(id); // 親クラスのコンストラクタを呼ぶ
}
// Loggable が要求する log() は自分で実装する
log(): void {
console.log(`${this.describe()} / name: ${this.name}`);
}
}
const user = new User(1, 'たろう');
user.log(); // Entity #1 / name: たろう
User は Entity から describe() の実装を受け継ぎつつ、Loggable が要求する log() は自分で書いています。ここで大切なのは、implements Loggable は log() の中身を用意してくれるわけではない、という点です。次の節で、この誤解について詳しく見ていきます。
implements は実装を提供しない
implements という言葉の響きから、「実装を用意してくれる」と誤解されがちですが、実際は逆です。interface はあくまで形(どんなメンバーがあるべきか)だけを定義したもので、メソッドの中身は一切持ちません。したがって、implements したクラスは、要求されたメソッドの中身をすべて自分で書く必要があります。implements がやってくれるのは「書き忘れがないかのチェック」だけです。
もし複数のクラスで共通の中身を使い回したいなら、それは interface の役目ではなく、extends による継承や抽象クラス(abstract class)の役目です。「形だけを強制したい」なら interface と implements、「共通の実装も配りたい」なら継承、と使い分けるのがポイントです。
private やコンストラクタは interface で強制できない
interface が約束させられるのは、あくまで外から見える(public な)形だけです。そのため、クラスの内部事情に関わる次のようなものは interface では強制できません。
まず private や protected なメンバーは、interface に書くことができません。interface は公開されている契約を表すものなので、非公開のメンバーを要求するという考え方自体が存在しないのです。同様に、コンストラクタの引数の形も interface(の implements)では強制できません。implements がチェックするのはインスタンスの形であって、そのインスタンスをどう生成するか(コンストラクタのシグネチャ)は対象外だからです。
interface HasValue {
value: number;
}
// implements がチェックするのは「インスタンスが value を持つか」だけ。
// コンストラクタの引数の形は自由に決められる。
class A implements HasValue {
constructor(public value: number) {}
}
class B implements HasValue {
value: number;
// 引数の形が A と違っても問題ない
constructor(initial: number, factor: number) {
this.value = initial * factor;
}
}
console.log(new A(10).value); // 10
console.log(new B(10, 3).value); // 30
A と B はコンストラクタの引数がまったく違いますが、どちらも「インスタンスが value: number を持つ」という点では HasValue を満たしているため、両方とも正しく実装できていると判断されます。「生成のしかた」まで縛りたい場合は、interface の implements ではなく、ファクトリ関数やコンストラクタ型(new (...) => T)といった別の手段を検討することになります。
implements しても構造的型付けは変わらない
もう1つ押さえておきたいのが、implements を付けても TypeScript の型の互換性の基準は「構造」のままだという点です。TypeScript は型どうしが互換かどうかを、宣言された名前ではなくプロパティやメソッドの形(構造)で判断します。これを構造的型付け(Structural Typing)と呼びます。implements はあくまでクラス定義時のチェックのためのものであって、そのクラスを「特別な仲間」として囲い込むわけではありません。
interface Animal {
name: string;
speak(): string;
}
class Dog implements Animal {
constructor(public name: string) {}
speak(): string {
return 'ワン';
}
}
// implements していないただのオブジェクトでも、
// 形が Animal と一致していれば Animal として扱える
const robot = {
name: 'ロボ',
speak: (): string => 'ビープ',
};
const animals: Animal[] = [new Dog('ポチ'), robot]; // どちらも OK
animals.forEach((a) => console.log(a.name, a.speak()));
robot は implements Animal と宣言していない、ただのオブジェクトリテラルですが、name と speak() という構造が Animal と一致しているため、Animal の配列にそのまま入れられます。逆に、implements Animal を付けた Dog だけが Animal になれる、というわけではありません。implements は「このクラスが約束を守っているか」を定義時に確認するためのものであり、実行時や代入時の型判定を名前ベースに変えるものではない、と理解しておくと混乱しません。
まとめ
implements は、クラスが特定の interface の形(プロパティとメソッド)を満たしていることを約束させ、実装漏れをコンパイル時に検出させるための仕組みです。class C implements A, B のように複数の interface をまとめて実装でき、extends による継承とも extends を先・implements を後の順で併用できます。一方で implements はメソッドの中身を提供してくれるわけではなく、実装はすべて自分で書く必要がある点、private/protected なメンバーやコンストラクタの引数は interface では強制できない点、そして implements を付けても TypeScript の互換判定は構造的型付けのままである点に注意しましょう。「形だけを強制したいなら interface と implements、共通の実装も配りたいなら継承」と役割を意識して使い分けると、設計がすっきりします。