本文へスキップ
ウェブエンジニア問題集
クラスとimplements

クラスとimplements — インターフェースを実装の契約にする

約19分
この章の目次開く

前章の最後に、interface は「オブジェクトの形状(とクラス)にしか使えない」と書きました。このカッコの中を回収するのがこの章です。

interface はオブジェクトリテラルの型を決めるだけの道具ではありません。クラスに対して「このプロパティとメソッドを必ず持て」と要求する契約としても使えます。それを宣言するキーワードが implements です。

学習者学習者

class User implements Person っていうコードを見たんだけど、const user: Person = ... と型注釈を書くのと何が違うの?

結論を先に言うと、implements はクラスの定義そのものを検査するための構文です。型注釈は「作り終えた値」を検査しますが、implements は「クラスを書いている最中」に不足を教えてくれます。この違いが実務でどう効くのかを、この章で見ていきます。


クラスの基本をおさらいする

クラスは「同じ形のオブジェクトを何個も作るための設計図」です。new で呼び出すと、その設計図どおりのオブジェクト(インスタンス)ができます。

class User {
  name: string;
  age: number;
 
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
 
  greet(): string {
    return `こんにちは、${this.name}さん`;
  }
}
 
const alice = new User('Alice', 25);
console.log(alice.greet()); // こんにちは、Aliceさん
tsx

JavaScriptとの違いは、冒頭の2行です。

name: string;
age: number;
tsx

TypeScriptでは、クラスが持つプロパティをあらかじめ宣言して型を付ける必要があります。この宣言がないと、this.name = name の時点で「User に name は存在しない」というエラーになります。

パラメータプロパティ — 宣言と代入をまとめる

先ほどのコードは、name という単語が3回出てきました。宣言・引数・代入です。同じ名前を3回書くのは冗長なので、TypeScriptには短縮構文があります。

class User {
  constructor(
    public name: string,
    public age: number,
  ) {}
}
 
const alice = new User('Alice', 25);
console.log(alice.name); // Alice
tsx

コンストラクタの引数にアクセス修飾子(public / private / protected / readonly)を付けると、同名のプロパティ宣言と代入を自動で行ってくれます。これをパラメータプロパティと呼びます。

アクセス修飾子を付けたときだけプロパティになります。修飾子なしの引数は、ただの引数のままです。
先生先生

public を付け忘れると「なぜかプロパティが生えない」と悩むことになる。短縮構文のスイッチは修飾子そのものだと覚えておこう。


implements — クラスに契約を課す

ここからが本題です。implements は、クラス宣言に「このインターフェースを満たしている」と宣言するためのキーワードです。

interface Greetable {
  name: string;
  greet(): string;
}
 
class User implements Greetable {
  constructor(public name: string) {}
 
  greet(): string {
    return `こんにちは、${this.name}さん`;
  }
}
tsx

User は Greetable が要求する name と greet() を両方持っているので、問題なくコンパイルされます。

では、greet() を書き忘れるとどうなるでしょうか。

class Robot implements Greetable {
  name = 'R2-D2';
}
tsx
エラー: Class 'Robot' incorrectly implements interface 'Greetable'.
  Property 'greet' is missing in type 'Robot' but required in type 'Greetable'.

エラーは Robot を「使った場所」ではなく、Robot を「定義した場所」に出ます。これが implements の価値です。

型注釈との違い

冒頭の学習者の疑問に答えます。implements を書かず、型注釈だけで済ませることもできます。

// implementsなし
class Robot {
  name = 'R2-D2';
}
 
const robot: Greetable = new Robot(); // ここでエラーになる
tsx

このコードもエラーにはなります。ただし、エラーが出るのは Robot を Greetable として使おうとした場所です。

契約内容を確認し合うイメージ
implementsは「作る側」に契約を課す

Robot を定義したファイルと、Robot を使うファイルが離れていると、この差は大きくなります。クラスを書いた本人はエラーに気づかず、数日後に別の担当者が使おうとして初めてエラーに遭遇する、ということが起こります。

エラーが出る場所気づくタイミング
型注釈だけクラスを使う場所使う人が使うとき
implementsクラスを定義する場所書いた人が書いているとき

実装漏れは「作る側」の責任なので、作る側に即座に知らせたほうが直しやすい、というのが implements を使う理由です。

学習者学習者

じゃあ implements を書けば、interface に書いたメソッドが自動で使えるようになるってこと?

いいえ、そこが最大の誤解ポイントです。次の節で説明します。


implementsは検査であって、継承ではない

implements が行うのはチェックだけです。インターフェースから実装をもらってくることは一切ありません。

interface Greetable {
  greet(): string;
}
 
class User implements Greetable {
  // greet() を自分で書かなければエラー。
  // Greetable は「持っているか」を確認するだけで、中身はくれない
}
tsx

インターフェースはそもそも実装(メソッドの中身)を持っていないので、渡せるものが何もないのです。

一方で、契約は複数同時に課せます。

interface Greetable {
  greet(): string;
}
 
interface Serializable {
  toJSON(): string;
}
 
class User implements Greetable, Serializable {
  constructor(public name: string) {}
 
  greet(): string {
    return `こんにちは、${this.name}さん`;
  }
 
  toJSON(): string {
    return JSON.stringify({ name: this.name });
  }
}
tsx

カンマ区切りで並べるだけです。「持っているかの確認」を増やしているだけなので、いくつ並べても衝突は起きません。


なぜ検査だけで済むのか — 型と値を分けて考える

「implements は検査だけ」と言われても、腑に落ちないかもしれません。ここを納得するには、TypeScriptが型と値をはっきり分けて扱っていることを知る必要があります。

学習者学習者

interface と class って、どっちも「オブジェクトの設計図」に見えるんだけど、何が違うの?

インターフェースは「型」でしかない

interface は、型に名前を付けているだけです。

interface B {
  x: number;
}
tsx

これは、次の型エイリアスとほぼ同じ意味です。

type B = { x: number };
tsx

インターフェースは値ではないので、実行時には存在しません。コンパイルすると完全に消えます。new B() はできませんし、console.log(B) も書けません。「x: number を持つ形」という説明書きが、コンパイラの中だけに存在している状態です。

クラスは「値」と「型」の両方を作る

ここが混乱の元です。class A {} と書くと、TypeScriptは2つのものを同時に定義します。

class A {
  x = 1;
}
 
const a: A = new A();
//       ↑型     ↑値
tsx
実体JSに残るか
値としての Anew A() で呼べるコンストラクタ✅ 残る
型としての A「x: number を持つオブジェクト」という型❌ 残らない

同じ A という名前ですが、const a: A の A と new A() の A は別物を指しています。型を書く位置に現れた A は型として、値を書く位置に現れた A は値として解釈されます。

先生先生

interface は型だけ、class は値と型の両方。この非対称さが分かると、implements の挙動はほとんど説明がつくよ。

implements Greetable が要求しているのは、クラスが作る「型のほう」が Greetable と噛み合うかです。Greetable は型でしかなく、渡せる実体を何も持っていません。だから検査しかできないのです。

型は名前ではなく構造で判定される

もう1つ、implements の性質を決めている仕組みがあります。TypeScriptの型は「名前が一致するか」ではなく「形が一致するか」で判定されます。これを構造的型付けと呼びます。

interface Greetable {
  greet(): string;
}
 
// implementsを書いていないクラス
class Cat {
  greet(): string {
    return 'にゃー';
  }
}
 
const g: Greetable = new Cat(); // OK。形が合っているので通る
tsx

Cat は Greetable の名前をどこにも書いていませんが、greet(): string を持っているので Greetable として扱えます。implements は「その型として使えるようにする」ためのものではありません。形さえ合っていれば、書かなくても使えてしまうのです。

学習者学習者

えっ、じゃあ implements って本当に要らない子なんじゃ…?

そうではありません。構造的型付けは「合っていれば通す」だけで、合っているかどうかを教えてはくれません。Cat から greet を消しても、Cat を定義した場所は静かなままで、const g: Greetable = new Cat() と書いた場所が初めて赤くなります。implements は、この判定をクラスの定義時点に前倒しするためのスイッチです。

型として使えるか実装漏れに気づく場所
implements なし✅ 形が合えば使える使う場所
implements あり✅ 形が合えば使える定義する場所

役割は「使えるようにすること」ではなく「意図の宣言と、その場での検証」だと捉えると正確です。


extendsとimplementsの違い

クラスに関わるキーワードには extends もあります。混同しやすいので整理します。

extendsimplements
対象クラス(親クラス)インターフェース
実装を受け継ぐか✅ 受け継ぐ❌ 受け継がない
個数1つだけ何個でも
コンパイル後のJS残る消える
super が使えるか✅ 使える❌ 使えない

extends はコードの再利用、implements は形の保証が目的だと考えると区別しやすくなります。

両方を同時に書くこともできます。順序は extends が先です。

class AdminUser extends User implements Serializable {
  // Userから実装を受け継ぎつつ、Serializableの契約も満たす
}
tsx

TypeScriptがクラスに足すもの

implements のほかにも、TypeScriptはクラスにいくつか機能を足しています。実務でよく見るものをまとめます。

アクセス修飾子

修飾子クラスの外から子クラスから備考
public✅✅省略時の既定
protected❌✅
private❌❌
class Account {
  constructor(
    public name: string,
    private balance: number,
  ) {}
 
  deposit(amount: number): void {
    this.balance += amount;
  }
}
 
const account = new Account('Alice', 1000);
account.name; // OK
account.balance; // エラー: Property 'balance' is private and only accessible within class 'Account'.
tsx

readonly — 初期化後は変更できない

class Config {
  constructor(readonly apiUrl: string) {}
}
 
const config = new Config('https://example.com');
config.apiUrl = 'https://evil.example.com'; // エラー: Cannot assign to 'apiUrl' because it is a read-only property.
tsx

readonly はコンストラクタの中でだけ代入できます。設定値やIDのように、生成後に変わってほしくない値に付けます。

privateと#privateの違い

JavaScript側にも、あとからプライベートフィールド(# で始まる名前)が追加されました。似ていますが、効き方が違います。

private(TypeScript)#(JavaScript)
検査のタイミングコンパイル時のみ実行時
JSに残るか消える残る
実行時に外から読めるか✅ 読めてしまう❌ 読めない
class A {
  private secret = 1; // 型チェックは通らないが、実行時は読める
  #realSecret = 2; // 実行時も読めない
}
tsx
先生先生

private はあくまで型レベルの約束事。本当に外部から隠したい値なら # を使おう。とはいえ普段のアプリ開発では private で十分なことがほとんどだよ。


abstract — 実装を持てる契約

interface は実装を持てませんが、「共通の実装は親に置きつつ、一部だけ子に強制したい」ことがあります。そのための仕組みが抽象クラスです。

abstract class Animal {
  constructor(protected name: string) {}
 
  // 実装を持つ(共通処理)
  introduce(): string {
    return `${this.name}です。${this.cry()}`;
  }
 
  // 実装を持たない(子クラスに強制する)
  abstract cry(): string;
}
 
class Dog extends Animal {
  cry(): string {
    return 'ワンワン';
  }
}
 
const dog = new Dog('ポチ');
console.log(dog.introduce()); // ポチです。ワンワン
 
new Animal('名前'); // エラー: Cannot create an instance of an abstract class.
tsx

abstract を付けたクラスは new できません。子クラスに継承されて、abstract メソッドが実装されてはじめて使えるようになります。

interfaceと抽象クラスの使い分け

interfaceabstract class
実装を持てるか❌✅
複数を組み合わせられるか✅ いくつでも❌ 継承は1つだけ
コンパイル後のJS消える残る
フィールドの初期値持てない持てる

共有したい実装があるなら抽象クラス、形だけ揃えたいなら interface を選びます。迷ったらまず interface から始め、共通処理の重複が実際に苦痛になってから抽象クラスへ移すのが安全です。


よくあるハマりどころ

想定外のエラーに慌てるイメージ

implements は「検査するだけ」という性質から、直感に反する挙動がいくつかあります。実務で遭遇しやすい順に並べます。

引数の型は推論されない

最も引っかかりやすい落とし穴です。

interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) {
    // s は暗黙のany。noImplicitAnyが有効ならエラーになる
    return s.toLowerCase() === 'ok';
  }
}
tsx

Checkable が check(name: string) と定めているのだから s は string に推論されそうですが、そうはなりません。前述のとおり implements はクラスの型を変えないため、s の型を決める材料にはならないのです。

// 引数の型は自分で書く
class NameChecker implements Checkable {
  check(s: string): boolean {
    return s.toLowerCase() === 'ok';
  }
}
tsx

省略可能プロパティは生えない

interface User {
  name: string;
  nickname?: string;
}
 
class Person implements User {
  name = 'Alice';
}
 
const p = new Person();
p.nickname; // エラー: Property 'nickname' does not exist on type 'Person'.
tsx

nickname は ? 付きなので実装しなくても契約は満たせます。しかし実装しなかった以上、Person にそのプロパティは存在しません。implements はクラスにメンバーを追加しないからです。

privateなメンバーで契約は満たせない

interface Greetable {
  greet(): string;
}
 
class User implements Greetable {
  private greet(): string {
    return 'こんにちは';
  }
}
tsx
エラー: Class 'User' incorrectly implements interface 'Greetable'.
  Property 'greet' is private in type 'User' but not in type 'Greetable'.

インターフェースのメンバーはすべて公開されている前提なので、実装側で隠すことはできません。

コンストラクタは検査されない

implements が検査するのはインスタンス側だけです。new のシグネチャを書いても意図通りには働きません。

interface WithFactory {
  new (name: string): User;
}
 
class User implements WithFactory {
  constructor(public name: string) {}
}
tsx
エラー: Class 'User' incorrectly implements interface 'WithFactory'.
  Type 'User' provides no match for the signature 'new (name: string): User'.

User のインスタンスが new シグネチャを持っているかを見にいくため、このエラーになります。コンストラクタの形を型で縛りたい場合は、implements ではなくクラスそのものを値として型注釈する方法をとります。

学習者学習者

ハマりどころが多いね…。結局、実務では implements を書いたほうがいいの?

書いたほうがよい場面ははっきりしています。同じインターフェースを満たすクラスが複数あり、差し替えて使う予定があるときです。APIクライアントの本番実装とモック実装、通知手段のメール版とSlack版、といったケースが典型です。逆に、そのクラスが1つしかなく差し替えの予定もないなら、implements を足す価値は小さくなります。


ちゃんと使うためのポイント

  • implements は検査だけを行う。インターフェースから実装もプロパティも受け取れない
  • エラーの発生場所が「使う側」から「作る側」に移るのが最大の利点
  • 実装側のメソッド引数には自分で型注釈を書く。推論は効かない
  • extends は実装の再利用で1つだけ、implements は形の保証で何個でも
  • 共通の実装を配りたいなら abstract class、形を揃えたいだけなら interface
  • パラメータプロパティは、アクセス修飾子を付けたときだけ有効になる
  • 差し替える予定のあるクラス(本番実装とモックなど)でこそ implements が効く
理解できて喜ぶイメージ

まとめ

class はJavaScriptの構文で、TypeScriptはそこに型注釈・アクセス修飾子・implements を足しています。implements は interface をクラスへの契約として使う仕組みで、実装漏れをクラスの定義時点で検出できます。

ただし implements が行うのは検査だけです。実装も型推論も与えてくれないため、メソッドの引数には自分で型を書く必要があります。

次の章では、TypeScriptの表現力を大きく広げるユニオン型とリテラル型を扱います。

参考リンク

  • TypeScript Handbook — Classes(英語) — implements 句・アクセス修飾子・パラメータプロパティ・abstract の公式な説明。この章で扱った落とし穴は「implements Clauses」のCautionsに記載がある
  • MDN — クラス — JavaScript側のクラス構文とプライベートフィールド(#)の仕様