はじめに

TypeScriptでアプリケーションを設計する際、interfaceclassの連携は非常に重要です。interfaceでオブジェクトの「契約」を定義し、classで具体的な実装を提供することで、疎結合で保守性の高いコードを実現できます。

本記事では、TypeScriptのimplementsキーワードを使ったinterfaceのクラス実装、クラス継承のextendsとの違い、複数interfaceの同時実装、staticメンバーの型定義、そしてジェネリッククラスとinterfaceの組み合わせについて詳しく解説します。

この記事を読み終えると、以下のことができるようになります。

  • implementsでinterfaceをクラスに実装できる
  • extendsimplementsの違いを理解し、適切に使い分けられる
  • 複数のinterfaceを1つのクラスに同時実装できる
  • staticメンバーとインスタンスメンバーの型定義を区別できる
  • ジェネリッククラスでinterfaceを活用した柔軟な設計ができる

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • interfaceで定義したメソッドを実装しないクラスがコンパイルエラーになる
  • implements句で型チェックが行われ、不足するメンバーが検出される
  • 複数interfaceを実装したクラスが各interfaceの型として扱える

TypeScriptのimplementsとは

implementsは、クラスが特定のinterfaceを満たすことを宣言するキーワードです。interfaceで定義されたプロパティやメソッドを、クラスが必ず実装することを保証します。

flowchart TB
    A["interface Printable"] --> B["print(): void"]
    C["class Document implements Printable"] --> D["print() メソッドを実装"]
    A -.->|"契約"| C
    B -.->|"実装を強制"| D

implementsの基本構文

interfaceを定義し、クラスでimplementsを使って実装する基本的な例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// interfaceの定義
interface Printable {
  print(): void;
}

// interfaceを実装するクラス
class Document implements Printable {
  constructor(private content: string) {}

  print(): void {
    console.log(`印刷中: ${this.content}`);
  }
}

const doc = new Document("重要な資料");
doc.print(); // 印刷中: 重要な資料

implementsを使うことで、DocumentクラスはPrintableインターフェースのprintメソッドを必ず実装しなければなりません。実装を忘れるとコンパイルエラーが発生します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface Printable {
  print(): void;
}

// エラー: Class 'BrokenDocument' incorrectly implements interface 'Printable'.
// Property 'print' is missing in type 'BrokenDocument' but required in type 'Printable'.
class BrokenDocument implements Printable {
  constructor(private content: string) {}
  // print() メソッドがない
}

implementsの重要な注意点

TypeScriptのimplementsは、クラスがinterfaceの型として扱えることをチェックするだけです。implementsによってクラスの型が変わるわけではありません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Checkable {
  check(name: string): boolean;
}

class NameChecker implements Checkable {
  // 注意: パラメータ s の型は自動的に推論されない
  check(s) {
    // Parameter 's' implicitly has an 'any' type.
    return s.toLowerCase() === "ok";
  }
}

implementsを使っても、メソッドの引数に型注釈は自動的に付与されません。明示的に型を指定する必要があります。

1
2
3
4
5
class NameChecker implements Checkable {
  check(s: string): boolean {
    return s.toLowerCase() === "ok";
  }
}

また、オプショナルプロパティを持つinterfaceを実装しても、そのプロパティがクラスに自動的に追加されるわけではありません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface HasOptional {
  required: string;
  optional?: number;
}

class MyClass implements HasOptional {
  required = "hello";
  // optionalは定義していない
}

const obj = new MyClass();
// obj.optional = 10; // エラー: Property 'optional' does not exist on type 'MyClass'.

オプショナルプロパティもクラスで使いたい場合は、明示的に定義する必要があります。

extendsとimplementsの違い

TypeScriptでは、extendsimplementsは異なる目的で使用されます。両者の違いを理解することは、適切なクラス設計の基礎となります。

extendsはクラスの継承

extendsは、クラスが別のクラスを継承するときに使用します。親クラスのプロパティやメソッドの実装がそのまま引き継がれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Animal {
  constructor(public name: string) {}

  move(): void {
    console.log(`${this.name}が移動しています`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.log(`${this.name}がワンワンと吠えています`);
  }
}

const dog = new Dog("ポチ");
dog.move(); // ポチが移動しています(親クラスのメソッドを継承)
dog.bark(); // ポチがワンワンと吠えています

implementsはinterfaceの実装

implementsは、クラスがinterfaceを実装するときに使用します。interfaceはメソッドのシグネチャ(名前、引数、戻り値の型)のみを定義し、実装は含みません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface Movable {
  move(): void;
}

class Robot implements Movable {
  constructor(public name: string) {}

  // Movableが要求するmove()を実装
  move(): void {
    console.log(`${this.name}が動いています`);
  }
}

const robot = new Robot("R2-D2");
robot.move(); // R2-D2が動いています

比較表

特徴 extends(クラス継承) implements(interface実装)
対象 クラス interface
実装の継承 継承される 継承されない(自分で実装)
複数指定 不可(単一継承) 可能(複数実装)
用途 コードの再利用 契約の定義・型チェック

extendsとimplementsの併用

クラスはextendsimplementsを同時に使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Flyable {
  fly(): void;
}

interface Swimmable {
  swim(): void;
}

class Animal {
  constructor(public name: string) {}
}

// クラスを継承しつつ、複数のinterfaceを実装
class Duck extends Animal implements Flyable, Swimmable {
  fly(): void {
    console.log(`${this.name}が飛んでいます`);
  }

  swim(): void {
    console.log(`${this.name}が泳いでいます`);
  }
}

const duck = new Duck("ドナルド");
duck.fly();  // ドナルドが飛んでいます
duck.swim(); // ドナルドが泳いでいます
classDiagram
    class Animal {
        +name: string
        +constructor(name: string)
    }
    class Flyable {
        <<interface>>
        +fly(): void
    }
    class Swimmable {
        <<interface>>
        +swim(): void
    }
    class Duck {
        +fly(): void
        +swim(): void
    }
    Animal <|-- Duck : extends
    Flyable <|.. Duck : implements
    Swimmable <|.. Duck : implements

複数interfaceの同時実装

TypeScriptでは、1つのクラスで複数のinterfaceを同時に実装できます。これにより、クラスに複数の役割(振る舞い)を持たせることができます。

基本的な複数interface実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
interface Readable {
  read(): string;
}

interface Writable {
  write(data: string): void;
}

interface Closable {
  close(): void;
}

class FileStream implements Readable, Writable, Closable {
  private data: string = "";
  private isClosed: boolean = false;

  read(): string {
    if (this.isClosed) {
      throw new Error("ストリームは閉じられています");
    }
    return this.data;
  }

  write(data: string): void {
    if (this.isClosed) {
      throw new Error("ストリームは閉じられています");
    }
    this.data += data;
  }

  close(): void {
    this.isClosed = true;
    console.log("ストリームを閉じました");
  }
}

const stream = new FileStream();
stream.write("Hello, ");
stream.write("World!");
console.log(stream.read()); // Hello, World!
stream.close();             // ストリームを閉じました

各interfaceの型として扱う

複数のinterfaceを実装したクラスのインスタンスは、各interfaceの型として扱えます。これは、関数の引数で特定の機能のみを要求する場合に便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Readableだけを必要とする関数
function printContent(reader: Readable): void {
  console.log(reader.read());
}

// Writableだけを必要とする関数
function saveData(writer: Writable, data: string): void {
  writer.write(data);
}

// Closableだけを必要とする関数
function cleanup(resource: Closable): void {
  resource.close();
}

const stream = new FileStream();
saveData(stream, "テストデータ");
printContent(stream); // テストデータ
cleanup(stream);      // ストリームを閉じました

interfaceの継承と実装の組み合わせ

interface同士もextendsで継承できます。複数のinterfaceを継承した新しいinterfaceを作り、それを実装することも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface Readable {
  read(): string;
}

interface Writable {
  write(data: string): void;
}

// 複数のinterfaceを継承した新しいinterface
interface ReadWritable extends Readable, Writable {
  // 追加のメソッドを定義することも可能
  reset(): void;
}

class Buffer implements ReadWritable {
  private data: string = "";

  read(): string {
    return this.data;
  }

  write(data: string): void {
    this.data += data;
  }

  reset(): void {
    this.data = "";
  }
}

const buffer = new Buffer();
buffer.write("初期データ");
console.log(buffer.read()); // 初期データ
buffer.reset();
console.log(buffer.read()); // (空文字)

staticメンバーの型定義

TypeScriptでは、クラスのインスタンスメンバーとstatic(静的)メンバーは別々に型付けされます。interfaceimplementsを使う場合、インスタンスメンバーのみがチェック対象となる点に注意が必要です。

staticメンバーはimplementsの対象外

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
interface HasId {
  id: string;
}

class MyClass implements HasId {
  // これはインスタンスプロパティ
  id: string;

  // これはstaticプロパティ(implementsのチェック対象外)
  static staticId: string = "STATIC_001";

  constructor(id: string) {
    this.id = id;
  }
}

const instance = new MyClass("INSTANCE_001");
console.log(instance.id);       // INSTANCE_001
console.log(MyClass.staticId); // STATIC_001

クラスのコンストラクタ側の型を定義する

staticメンバーやコンストラクタの型を定義したい場合は、別のinterfaceを作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// インスタンス側の型
interface ClockInterface {
  tick(): void;
}

// コンストラクタ(クラス自体)の型
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
  readonly defaultTimezone: string;
}

// ファクトリ関数でクラスとコンストラクタ型を検証
function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  console.log(`Timezone: ${ctor.defaultTimezone}`);
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  static readonly defaultTimezone: string = "Asia/Tokyo";

  constructor(
    private hour: number,
    private minute: number
  ) {}

  tick(): void {
    console.log(`${this.hour}:${this.minute.toString().padStart(2, "0")}`);
  }
}

const clock = createClock(DigitalClock, 14, 30);
clock.tick(); // 14:30

staticメソッドの型付け

staticメソッドの型付けも、インスタンスメソッドと同様に行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MathHelper {
  static readonly PI: number = 3.14159;

  static add(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }

  static circleArea(radius: number): number {
    return MathHelper.PI * radius ** 2;
  }
}

console.log(MathHelper.add(5, 3));         // 8
console.log(MathHelper.multiply(4, 7));    // 28
console.log(MathHelper.circleArea(10));    // 314.159

typeof を使ったstaticメンバーの型取得

クラスのstaticメンバーを含む型を取得するには、typeofを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Logger {
  static level: "debug" | "info" | "error" = "info";
  static prefix: string = "[LOG]";

  static log(message: string): void {
    console.log(`${Logger.prefix} ${message}`);
  }
}

// クラス自体の型を取得
type LoggerStatic = typeof Logger;

// staticメンバーを含む型として使用できる
function configureLogger(logger: LoggerStatic): void {
  logger.level = "debug";
  logger.prefix = "[DEBUG]";
}

configureLogger(Logger);
Logger.log("設定が変更されました"); // [DEBUG] 設定が変更されました

ジェネリッククラスとinterfaceの組み合わせ

ジェネリクスを使うことで、さまざまな型に対応できる柔軟なクラス設計が可能になります。interfaceとジェネリッククラスを組み合わせることで、型安全かつ再利用性の高いコードを実現できます。

ジェネリックなinterfaceの定義

1
2
3
4
5
6
interface Repository<T> {
  findById(id: string): T | undefined;
  findAll(): T[];
  save(item: T): void;
  delete(id: string): boolean;
}

ジェネリッククラスでinterfaceを実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface Entity {
  id: string;
}

interface Repository<T extends Entity> {
  findById(id: string): T | undefined;
  findAll(): T[];
  save(item: T): void;
  delete(id: string): boolean;
}

class InMemoryRepository<T extends Entity> implements Repository<T> {
  private items: Map<string, T> = new Map();

  findById(id: string): T | undefined {
    return this.items.get(id);
  }

  findAll(): T[] {
    return Array.from(this.items.values());
  }

  save(item: T): void {
    this.items.set(item.id, item);
  }

  delete(id: string): boolean {
    return this.items.delete(id);
  }
}

具体的な型でジェネリッククラスを使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface User extends Entity {
  id: string;
  name: string;
  email: string;
}

interface Product extends Entity {
  id: string;
  name: string;
  price: number;
}

// User用のリポジトリ
const userRepo = new InMemoryRepository<User>();
userRepo.save({ id: "1", name: "田中太郎", email: "tanaka@example.com" });
userRepo.save({ id: "2", name: "佐藤花子", email: "sato@example.com" });

console.log(userRepo.findById("1")); // { id: "1", name: "田中太郎", email: "tanaka@example.com" }
console.log(userRepo.findAll().length); // 2

// Product用のリポジトリ
const productRepo = new InMemoryRepository<Product>();
productRepo.save({ id: "p1", name: "ノートPC", price: 150000 });
productRepo.save({ id: "p2", name: "マウス", price: 3000 });

console.log(productRepo.findById("p1")); // { id: "p1", name: "ノートPC", price: 150000 }

ジェネリック制約とinterfaceの活用

ジェネリクスに制約(extends)を付けることで、型パラメータが特定のinterfaceを満たすことを保証できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
interface Identifiable {
  id: string;
}

interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

// 複数のinterfaceを制約として指定
interface AuditableRepository<T extends Identifiable & Timestamped> {
  save(item: T): void;
  findRecent(since: Date): T[];
}

interface AuditedEntity extends Identifiable, Timestamped {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface AuditedUser extends AuditedEntity {
  name: string;
  email: string;
}

class AuditedUserRepository implements AuditableRepository<AuditedUser> {
  private items: AuditedUser[] = [];

  save(item: AuditedUser): void {
    item.updatedAt = new Date();
    const existingIndex = this.items.findIndex((i) => i.id === item.id);
    if (existingIndex >= 0) {
      this.items[existingIndex] = item;
    } else {
      item.createdAt = new Date();
      this.items.push(item);
    }
  }

  findRecent(since: Date): AuditedUser[] {
    return this.items.filter((item) => item.updatedAt >= since);
  }
}

実践的なinterface設計パターン

ここでは、実際の開発で役立つinterface設計のパターンを紹介します。

依存性の注入(Dependency Injection)

interfaceを使うことで、具体的な実装に依存しない疎結合なコードを書けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 抽象化されたinterface
interface Logger {
  log(message: string): void;
  error(message: string): void;
}

interface Database {
  query(sql: string): Promise<unknown[]>;
  execute(sql: string): Promise<void>;
}

// interfaceに依存するサービスクラス
class UserService {
  constructor(
    private logger: Logger,
    private database: Database
  ) {}

  async createUser(name: string, email: string): Promise<void> {
    this.logger.log(`Creating user: ${name}`);
    try {
      await this.database.execute(
        `INSERT INTO users (name, email) VALUES ('${name}', '${email}')`
      );
      this.logger.log(`User created successfully: ${name}`);
    } catch (err) {
      this.logger.error(`Failed to create user: ${name}`);
      throw err;
    }
  }
}

// 本番用の実装
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[INFO] ${message}`);
  }

  error(message: string): void {
    console.error(`[ERROR] ${message}`);
  }
}

// テスト用のモック実装
class MockLogger implements Logger {
  logs: string[] = [];
  errors: string[] = [];

  log(message: string): void {
    this.logs.push(message);
  }

  error(message: string): void {
    this.errors.push(message);
  }
}

ファクトリパターン

interfaceを返すファクトリを使うことで、実装の詳細を隠蔽できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
interface PaymentProcessor {
  processPayment(amount: number): Promise<boolean>;
  refund(transactionId: string): Promise<boolean>;
}

class CreditCardProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<boolean> {
    console.log(`クレジットカードで${amount}円を処理中...`);
    return true;
  }

  async refund(transactionId: string): Promise<boolean> {
    console.log(`取引 ${transactionId} を返金中...`);
    return true;
  }
}

class PayPalProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<boolean> {
    console.log(`PayPalで${amount}円を処理中...`);
    return true;
  }

  async refund(transactionId: string): Promise<boolean> {
    console.log(`PayPal取引 ${transactionId} を返金中...`);
    return true;
  }
}

// ファクトリ関数
function createPaymentProcessor(type: "credit" | "paypal"): PaymentProcessor {
  switch (type) {
    case "credit":
      return new CreditCardProcessor();
    case "paypal":
      return new PayPalProcessor();
    default:
      throw new Error(`Unknown payment type: ${type}`);
  }
}

// 使用例
const processor = createPaymentProcessor("credit");
processor.processPayment(5000);

まとめ

本記事では、TypeScriptのinterface実装について詳しく解説しました。

implementsキーワードを使うことで、クラスが特定のinterfaceを満たすことを保証でき、型安全なコードを実現できます。extendsがクラス間の継承(実装の再利用)であるのに対し、implementsは契約の遵守(型の保証)を目的としています。

TypeScriptのinterfaceimplementsを活用した疎結合な設計は、以下のメリットをもたらします。

  • テストしやすいコード(モックへの差し替えが容易)
  • 変更に強い設計(実装を変えてもinterfaceが同じなら影響が限定的)
  • 明確な契約(クラスが満たすべき要件が型として表現される)

ジェネリッククラスと組み合わせることで、さらに柔軟で再利用性の高い設計が可能になります。ぜひ実際のプロジェクトで活用してみてください。

参考リンク