【入門編】Interfaceを用いた「依存性の注入(DI)」の型定義戦略 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。
フロントエンドからバックエンドまで、型システムの恩恵を最大限に受けて「絶対に壊れない堅牢なシステム」を作るのって、本当にワクワクしますよね。

今回は、中規模から大規模なアプリケーション開発において避けて通れない「依存性の注入(DI:Dependency Injection)」と、その核心を支える「インターフェース(Interface)の型定義戦略」についてお話ししていきます。

ほかのオブジェクト指向言語(JavaやC#など)からやってきた方はもちろん、「TypeScriptで綺麗で拡張しやすいコードを書けるようになりたい!」という方にとって、今日の話は間違いなく大きなターニングポイントになります。

ここをクリアすれば、あなたの設計力はワンランクもツーランクもアップしますよ。一緒にマスターしていきましょう!

—

1. なぜ「依存性の注入(DI)」と「Interface」が必要なのか?

まずは、私たちが直面する「よくある現実」からお話しますね。

例えば、ユーザーデータをデータベースに保存する `UserService` というクラスを作るとします。この時、もし以下のように直接「具体的なデータベース操作クラス」をコードの中に組み込んで(ハードコーディングして)しまったらどうなるでしょうか?

// ❌ 悪い例:具体的な実装にベッタリ依存している状態
class MySQLDatabase {
save(data: string) {
console.log(`MySQLに保存しました: ${data}`);
}
}

class UserService {
private db = new MySQLDatabase(); // 直接インスタンス化しちゃっている

register(username: string) {
this.db.save(username);
}
}

このコードの何が問題か分かりますか?
将来的に「やっぱりデータベースをPostgreSQLに変えたい!」あるいは「テストの時だけ、メモリ上で動くモックのデータベースに差し替えたい!」と思った時、`UserService` のコードをごっそり書き換えなければならなくなりますよね。

これを解決するのが DI(依存性の注入) です。
「具体的なモノ(MySQLなど)」に依存するのではなく、「こういうルール(振る舞い)を満たしていれば何でもいいよ」という『契約書(Interface)』にだけ依存するように設計するんです。

イメージとしてはこんな感じです:

[UserService] ──(使う)──> 【 IRepository (Interface: 契約書) 】
▲
┌──────┴──────┐
[MySQLImpl] [PostgreSQLImpl]
(具象クラスA) (具象クラスB)

`UserService` は、自分が使うものが MySQL なのか PostgreSQL なのかを知りません。「`IRepository` というルールさえ守っていれば、中身は何でもいいや」という状態で外から渡してもらう(注入してもらう)。これが依存性の注入の本質です。

—

2. TypeScriptにおけるInterfaceの基本と型安全なDIの実装

それでは、実際にTypeScriptでこの設計をコードに落とし込んでみましょう。
TypeScriptの `interface` は、JavaScriptにコンパイルすると消え去る「純粋な型情報のレイヤー」です。実行時のオーバーヘッドをゼロにしつつ、開発時に最高の型安全性をもたらしてくれます。

以下のコードをじっくり見てみてください。

/

  • 1. データベースアクセスの「契約(Interface)」を定義する
  • ここには「どんなメソッドを持ち、何を返すべきか」の型定義だけを書きます。

/
interface IRepository {
save(data: string): void;
}

/

  • 2. 具象クラスA:MySQL用の実装
  • implementsキーワードを使い、「このクラスはIRepositoryの契約を守ります」と宣言します。

/
class MySQLRepository implements IRepository {
public save(data: string): void {
console.log(`[MySQL]: “${data}” を安全に永続化しました。`);
}
}

/

  • 3. 具象クラスB:テスト用のモック実装(実際にDBには繋がない)

/
class MockRepository implements IRepository {
public save(data: string): void {
console.log(`[Mock]: テスト用メモリに “${data}” を一時保存しました。`);
}
}

/

  • 4. 依存性を注入されるメインのサービス
  • 具体的なクラスではなく、抽象である「IRepository」の型を受け取ります。

/
class UserService {
// コンストラクタの引数の型に Interface を指定する(これがDIのキモ!)
constructor(private repository: IRepository) {}

public registerUser(name: string): void {
if (name.length < 3) { throw new Error("ユーザー名は3文字以上必要です。"); } // 内部でどういうDBが使われているか知る必要がない this.repository.save(name); console.log(`ユーザー「${name}」の登録処理が完了しました。\n`); } } // --- 実際に動かしてみましょう --- // 本番環境のつもり const realDb = new MySQLRepository(); const productionService = new UserService(realDb); productionService.registerUser("Alice"); // 出力: // [MySQL]: "Alice" を安全に永続化しました。 // ユーザー「Alice」の登録処理が完了しました。 // テスト環境のつもり(具象クラスを差し替えるだけでOK!) const testDb = new MockRepository(); const testService = new UserService(testDb); testService.registerUser("Bob"); // 出力: // [Mock]: テスト用メモリに "Bob" を一時保存しました。 // ユーザー「Bob」の登録処理が完了しました。

このコードの素晴らしいポイント

`UserService` の中身を一切変更することなく、コンストラクタに渡すオブジェクト(`realDb` と `testDb`)を替えるだけで、振る舞いを完全に切り替えることができました。
TypeScriptのコンパイラは、渡されたオブジェクトが `IRepository` の契約をきちんと満たしているかを静的にチェックするため、間違ったオブジェクトを渡そうとするとコンパイルエラーで事前に止めてくれます。

—

3. 初学者が陥りがちな文法エラーとアンチパターン

さて、ここまで順調に見えますが、TypeScriptでDIやInterfaceを使い始めるときに、誰もが一度はハマる「罠」があります。気をつけてほしいポイントをいくつかシェアしますね。

罠その1:Interfaceを値として使おうとしてしまう

よくあるエラーがこれです。

interface IRepository {
save(data: string): void;
}

// ❌ 誤り:Interfaceは「型」なので、JavaScriptの「値(インスタンス)」としては使えません
const db = new IRepository(); // 💥 Error: ‘IRepository’ はインターフェイスですが、値として使用されています。

【解説】
`interface` はコンパイル後には消えてしまうゴーストのようなものです。`new` できるのは、実体(クラスやオブジェクト)だけ。
「インターフェースは設計図、クラスはそれを元に作った実物」というイメージをしっかり持っておきましょう。

罠その2:構造的型付け(Duck Typing)への勘違い

TypeScriptは「名前」ではなく「構造(中身)」で型を判断します(構造的型付け)。そのため、明示的に `implements` を書いていなくても、同じメソッドを持っていればInterfaceの代わりとして通ってしまいます。

interface IRepository {
save(data: string): void;
}

// implements IRepository を書き忘れている!
class LooseRepository {
public save(data: string): void {
console.log(data);
}
}

// TypeScriptの型チェックではエラーになりません(構造が一致するため)
const service = new UserService(new LooseRepository());

一見便利に見えますが、大規模開発やチーム開発では `implements` を書き忘れると「このクラスは何の契約を実装しているのか」がコード上から読み取りづらくなります。
意図したインターフェースの契約を守らせるために、原則として `implements` は明示的に記述する癖をつけましょう。

—

まとめ:InterfaceによるDIは型システムの最大の武器

今回は、Interfaceを使った依存性の注入(DI)の戦略について解説しました。

  • Interfaceは「契約書」:具象クラスではなく、抽象に依存することでコードの結合度を下げる。
  • DI(依存性の注入):外部から必要なオブジェクトを渡すことで、テストや仕様変更に強い柔軟なアーキテクチャを作る。
  • TypeScriptの恩恵:静的な型チェックにより、安全な部品の差し替えをコンパイル時に保証する。

最初は「クラスをわざわざインターフェースで包むなんて、コード量が増えて面倒だな」と感じるかもしれません。しかし、アプリが成長し、テストコードを書く必要が出てきたり、チームで開発したりするようになった時、この設計があなたを救う最強の盾になってくれます。

ここをマスターしたあなたは、もうただの「動くコードを書く人」ではありません。「保守性と拡張性をデザインできる優れたエンジニア」への一歩を確実に踏み出していますよ。

それでは、次のTypeScriptの冒険でお会いしましょう!

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