ゴール:「implements は“この約束は必ず守ります”という宣言」だと理解する
implements は一言でいうと、
「このクラスは、この interface が決めた“形(契約)”を必ず守ります」
と宣言するためのキーワードです。
継承(extends)が「中身ごと引き継ぐ」のに対して、implements は「中身は自分で書くけど、外から見える顔はこの通りにします」と約束するイメージです。
interface と implements の関係をまず押さえる
interface は「クラスの外側の“型”だけを決める」
まず、interface から。
interface UserLike {
id: number;
name: string;
greet(message: string): void;
}
TypeScriptこれは、
id: numberを持っているname: stringを持っているgreet(message: string): voidというメソッドを持っている
という「形(型)」だけを表しています。
中身(どう挨拶するか)は一切決めません。
ここで大事なのは、
interface は「こういうメンバーを持っていてね」という“約束の型”
だということです。
implements は「その約束をクラスに守らせる」
この interface をクラスに適用するのが implements です。
class User implements UserLike {
constructor(
public id: number,
public name: string
) {}
greet(message: string): void {
console.log(`${message}、${this.name}です`);
}
}
TypeScriptclass User implements UserLike と書いた瞬間に、
UserはUserLikeのすべてのプロパティ・メソッドを
「型どおりに」持っていなければならない- 1つでも足りなかったり、型が違ったりするとコンパイルエラー
という制約がかかります。
つまり implements は、
「このクラスは、この interface が決めた“顔”を必ず持ちます」
という“自己申告+コンパイル時チェック”の仕組みです。
implements が具体的に何をチェックしているか
プロパティ・メソッドの「名前・型」が一致しているか
例えば、次の interface があるとします。
interface HasId {
id: number;
}
TypeScriptこれを implements するクラスは、id: number を必ず持たなければいけません。
class Entity implements HasId {
constructor(public id: number) {}
}
TypeScriptもし、型を変えたり、名前を変えたりするとエラーになります。
class BadEntity implements HasId {
// id が string なので NG
// constructor(public id: string) {}
// そもそも id プロパティがないのも NG
}
TypeScriptここで重要なのは、
「interface が“外から見える約束”を決めていて、
クラスはその約束を破れない」
という関係です。
public なメンバーだけが対象になる
implements でチェックされるのは、基本的に public なメンバーです。
interface Named {
name: string;
}
class Person implements Named {
// public なので OK
constructor(public name: string) {}
// private name: string; // こうすると Named を満たさないことになる
}
TypeScriptinterface は「外から見える顔」を決めるものなので、private や protected は関係ありません。
「このクラスを外からどう扱えるか」を縛るのが implements
と捉えると、感覚がつかみやすくなります。
implements の一番おいしいところ:「具体クラスではなく interface に依存できる」
呼び出し側は「interface 型だけ知っていればいい」
implements の真価は、呼び出し側のコードに出ます。
interface Notifier {
send(message: string): void;
}
class EmailNotifier implements Notifier {
send(message: string): void {
console.log("メール送信:", message);
}
}
class LineNotifier implements Notifier {
send(message: string): void {
console.log("LINE送信:", message);
}
}
function notifyAll(notifiers: Notifier[], message: string) {
for (const n of notifiers) {
n.send(message);
}
}
const list: Notifier[] = [
new EmailNotifier(),
new LineNotifier(),
];
notifyAll(list, "こんにちは");
TypeScriptここでの構造を言葉にすると、
- interface
Notifierが「send(message: string): voidを持つもの」という契約を定義 EmailNotifierとLineNotifierは、その契約をimplementsで守るnotifyAll関数は「Notifierであれば何でもいい」として書ける- 実際にどのクラスを渡すかは、呼び出し側が自由に決められる
つまり、notifyAll は
「メールか LINE か」には依存しておらず、
「send できるかどうか」という interface にだけ依存しています。
implements は、「クラスに契約を守らせることで、
呼び出し側を“具体クラス”から解放する」ための仕組み
だと言えます。
実装の差し替えが“壊れにくく”なる
あとから Slack 通知を追加したくなったとします。
class SlackNotifier implements Notifier {
send(message: string): void {
console.log("Slack送信:", message);
}
}
TypeScriptこのとき、notifyAll 関数は一切変更不要です。Notifier の契約を守っている限り、
新しいクラスをいくらでも差し込めます。
implements でクラスを縛っておくと、
- 「この枠(interface)さえ守ってくれれば、中身は自由に変えていい」
- 「テスト用のダミー実装も簡単に差し込める」
という状態を作れます。
extends と implements の違いを、感覚で整理する
extends は「中身ごと引き継ぐ」、implements は「形だけ合わせる」
継承(extends)と implements は、よく混ざるポイントなので、
感覚で整理しておきます。
class Animal {
move() {
console.log("動いた");
}
}
class Dog extends Animal {
bark() {
console.log("ワン");
}
}
TypeScriptextends は、
- 親クラスのプロパティ・メソッドの「中身ごと」引き継ぐ
- 親の実装をそのまま使ったり、オーバーライドしたりできる
一方、implements はこうです。
interface Runner {
run(): void;
}
class Person implements Runner {
run(): void {
console.log("人が走る");
}
}
TypeScriptimplements は、
- interface が決めた「メソッドの形」だけを守る
- 中身はクラスごとに完全に自由に書く
- 実装の共有は一切しない
「実装を共有したいなら extends、
“こういうメソッドを持っていてほしい”だけなら implements」
という分け方で覚えておくと、迷いにくくなります。
実務での「implements の役割」まとめ
依存を「具体クラス」ではなく「契約(interface)」に向ける
例えば、サービスクラスがログ出力を使うケース。
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log("[LOG]", message);
}
}
class UserService {
constructor(private logger: Logger) {}
createUser(name: string): void {
this.logger.log(`ユーザー作成: ${name}`);
}
}
TypeScriptここでのポイントは、
UserServiceはConsoleLoggerという具体クラスには依存していない- 依存しているのは「
log(message: string): voidを持つもの」という interface だけ - あとから
FileLoggerやTestLoggerを作って差し替えるのも簡単
implements は、
「このクラスは Logger という契約を守ります」と宣言させることで、
他のコードが“Logger という約束”だけを信じて書けるようにする
ためのキーワードです。
まとめ:implements を自分の言葉で言い直すと
最後に、あなた自身の言葉でこう整理してみてください。
- interface は「クラスの外側の顔(プロパティ・メソッドの型)」を決める契約
implementsは「このクラスはその契約を必ず守ります」という宣言- 守れていなければコンパイルエラーになるので、“約束違反”を早期に防げる
- 呼び出し側は「具体クラス」ではなく「interface 型」に依存して書ける
- その結果、実装の差し替え・テストがしやすくなる
もし今、何かのクラスを直接 new してベタッと使っているコードがあれば、
「このクラスに interface をかませて、implements させるとどう変わるだろう?」
と一度だけでいいので想像してみてください。
そこから、
“クラスにべったり依存するコード”が、“契約(interface)を軸にした設計”に
少しずつシフトしていきます。
