TypeScript | 関数・クラス・ジェネリクス:クラス設計 – implementsの役割

TypeScript TypeScript
スポンサーリンク

ゴール:「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}です`);
  }
}
TypeScript

class User implements UserLike と書いた瞬間に、

  • UserUserLike のすべてのプロパティ・メソッドを
    「型どおりに」持っていなければならない
  • 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 を満たさないことになる
}
TypeScript

interface は「外から見える顔」を決めるものなので、
privateprotected は関係ありません。

「このクラスを外からどう扱えるか」を縛るのが 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 を持つもの」という契約を定義
  • EmailNotifierLineNotifier は、その契約を 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("ワン");
  }
}
TypeScript

extends は、

  • 親クラスのプロパティ・メソッドの「中身ごと」引き継ぐ
  • 親の実装をそのまま使ったり、オーバーライドしたりできる

一方、implements はこうです。

interface Runner {
  run(): void;
}

class Person implements Runner {
  run(): void {
    console.log("人が走る");
  }
}
TypeScript

implements は、

  • 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

ここでのポイントは、

  • UserServiceConsoleLogger という具体クラスには依存していない
  • 依存しているのは「log(message: string): void を持つもの」という interface だけ
  • あとから FileLoggerTestLogger を作って差し替えるのも簡単

implements は、

「このクラスは Logger という契約を守ります」と宣言させることで、
他のコードが“Logger という約束”だけを信じて書けるようにする

ためのキーワードです。


まとめ:implements を自分の言葉で言い直すと

最後に、あなた自身の言葉でこう整理してみてください。

  • interface は「クラスの外側の顔(プロパティ・メソッドの型)」を決める契約
  • implements は「このクラスはその契約を必ず守ります」という宣言
  • 守れていなければコンパイルエラーになるので、“約束違反”を早期に防げる
  • 呼び出し側は「具体クラス」ではなく「interface 型」に依存して書ける
  • その結果、実装の差し替え・テストがしやすくなる

もし今、何かのクラスを直接 new してベタッと使っているコードがあれば、
「このクラスに interface をかませて、implements させるとどう変わるだろう?」
と一度だけでいいので想像してみてください。

そこから、
“クラスにべったり依存するコード”が、“契約(interface)を軸にした設計”
少しずつシフトしていきます。

タイトルとURLをコピーしました