ゴール:「interface は“クラスに守らせる約束”」だと理解する
ここでのテーマは、
「interface を使って、クラスに“こういう形であれ”と約束させる」
という感覚をつかむことです。
クラスの継承(extends)は「中身も含めて引き継ぐ」仕組みでしたが、
interface は「中身はどうでもいいけど、外から見える“形”だけは守ってね」という契約です。
「クラスを縛る」というのは、
この“契約”をクラスに課す、という意味だと思ってください。
interface は「クラスの外側の顔」を決める契約
interface の基本:形だけを決める
まずはシンプルな interface から。
interface UserLike {
id: number;
name: string;
greet(): void;
}
TypeScriptこれは、
id: numberを持っていてname: stringを持っていてgreet(): voidというメソッドを持っている
という「形」を表しています。
ここには中身(実装)はありません。
「こういうプロパティ・メソッドを持っていてね」という“外側の顔”だけを決めています。
クラスに「この interface を実装しろ」と約束させる
クラス側でこの interface を「守ります」と宣言するのが implements です。
class User implements UserLike {
constructor(
public id: number,
public name: string
) {}
greet(): void {
console.log(`こんにちは、${this.name}です`);
}
}
TypeScriptここでのポイントは、
class User implements UserLikeと書いた瞬間に
「User は UserLike の形を必ず満たさなければならない」という制約がかかるid,name,greetが揃っていないとコンパイルエラーになる
ということです。
つまり interface は、
「このクラスは、こういうプロパティ・メソッドを必ず持っているはずだ」
という“約束の型”を定義し、implements でクラスにその約束を守らせる仕組みです。
interface でクラスを縛るメリット
呼び出し側は「interface だけ見ればいい」
interface でクラスを縛る一番のメリットは、
呼び出し側が「具体的なクラス名を意識しなくてよくなる」ことです。
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であれば何でもいい」として書ける
つまり、呼び出し側は
「メールか LINE か」には興味がなく、
「send できるかどうか」だけを気にすればよくなります。
「具体クラスではなく、interface を相手にコードを書く」
これが、クラスを interface で縛る最大の価値です。
実装を差し替えやすくなる
例えば、あとから Slack 通知を追加したくなったとします。
class SlackNotifier implements Notifier {
send(message: string): void {
console.log("Slack送信:", message);
}
}
TypeScriptこのとき、notifyAll 関数は一切変更不要です。Notifier の契約を守っている限り、
新しいクラスをいくらでも差し込めます。
「interface でクラスを縛る」というのは、
「この枠(interface)さえ守ってくれれば、中身は自由に差し替えていいよ」
という設計にする、ということです。
implements のルールと型の一致
プロパティ・メソッドの型は完全に一致させる
implements するとき、
クラス側は interface の型をきちんと満たしていなければいけません。
interface UserLike {
id: number;
name: string;
greet(message: string): void;
}
class User implements UserLike {
constructor(
public id: number,
public name: string
) {}
greet(message: string): void {
console.log(`${message}、${this.name}です`);
}
}
TypeScriptもし、型を変えようとするとエラーになります。
class BadUser implements UserLike {
id: number = 0;
name: string = "no name";
// 戻り値の型を変えたり、引数を変えたりすると NG
// greet(): void { ... } // 引数が足りない
// greet(message: string): string // 戻り値が違う
}
TypeScriptここで大事なのは、
「interface が“外から見た約束”を決めていて、
クラスはその約束を破れない」
という関係です。
public なメンバーだけが interface の対象になる
implements でチェックされるのは、基本的に public なメンバーです。
interface HasId {
id: number;
}
class Entity implements HasId {
// public なので OK
constructor(public id: number) {}
// private id: number; // こうすると HasId を満たさないことになる
}
TypeScriptinterface は「外から見える顔」を決めるものなので、private や protected は関係ありません。
「このクラスを外からどう扱えるか」を決めるのが interface
と覚えておくと、感覚がつかみやすくなります。
抽象クラスとの違いと、使い分けの感覚
interface は「形だけ」、抽象クラスは「形+共通ロジック」
同じ「クラスを縛る」でも、抽象クラスとは役割が違います。
interface:
interface Shape {
area(): number;
}
TypeScript- 中身は書けない
- 「こういうメソッドを持っていてね」という“形”だけ
抽象クラス:
abstract class ShapeBase {
constructor(public color: string) {}
abstract area(): number;
describe(): void {
console.log(`色: ${this.color}, 面積: ${this.area()}`);
}
}
TypeScript- 抽象メソッド(中身なし)も
- 具体メソッド(中身あり)も持てる
- 共通ロジックをここに書いて“共有”できる
interface でクラスを縛るときは、
「共通ロジックは別にいらない。
ただ、“こういうメソッドを持っているもの”として扱いたい」
という場面が向いています。
「継承ツリーを作りたくないとき」は interface が軽い
抽象クラスを使うと、
クラス同士が継承関係で強く結びつきます。
一方、interface は「実装の共有」をしないぶん、
クラス同士の結びつきが弱く、差し替えやすいです。
例えば、
「外部ライブラリのクラスにも同じ interface を実装させたい」
「テスト用のダミークラスを簡単に差し込みたい」
といったとき、interface で縛っておくとかなり楽になります。
「共通の親クラスを持たせたいのか?
それとも、バラバラなクラスに“同じ顔”だけさせたいのか?」
ここが、抽象クラスと interface の分かれ目です。
実務での「interface でクラスを縛る」典型パターン
依存を「具体クラス」ではなく「interface」に向ける
例えば、ログ出力の仕組みを考えます。
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log("[LOG]", message);
}
}
class FileLogger implements Logger {
log(message: string): void {
console.log("[FILE]", message);
}
}
class UserService {
constructor(private logger: Logger) {}
createUser(name: string): void {
this.logger.log(`ユーザー作成: ${name}`);
}
}
TypeScriptここでの設計のキモは、
UserServiceはConsoleLoggerやFileLoggerという“具体クラス”には依存していない- 依存しているのは「
log(message: string): voidを持つもの」という interface だけ - 実際にどの Logger を渡すかは、使う側が決められる
ということです。
これがまさに、
「interface でクラスを縛っておくことで、
依存先を“具体クラス”ではなく“契約”に向ける」
という設計です。
まとめ:interface でクラスを縛る、を自分の言葉で整理すると
最後に、あなた自身の言葉でこうまとめてみてください。
- interface は「クラスの外側の顔(プロパティ・メソッドの形)」を決める契約
implementsと書いたクラスは、その契約を必ず守らなければならない- 呼び出し側は「具体クラス」ではなく「interface 型」を相手にコードを書ける
- その結果、実装を差し替えやすくなり、テストもしやすくなる
- 抽象クラスが「形+共通ロジック」なのに対して、interface は「形だけ」
今あなたのコードの中で、
「特定のクラスにベタッと依存している処理」
(例:new ConsoleLogger() をあちこちで直接書いている)
があれば、それを
- interface を定義して
- クラスに
implementsさせて - 呼び出し側は interface 型を受け取る
という形にリファクタリングできないか、1カ所だけでいいので考えてみてください。
その一歩から、
“具体クラスに縛られたコード”が、“契約(interface)を軸にした柔らかい設計”に変わっていきます。
