はじめてのTypeScriptプログラミング入門 30日コース | Generics・Utility Types・Class・Module - Day 23:Generics実践

TypeScript 30日で身につけるTypeScript
スポンサーリンク
スポンサーリンク
  1. Day 23:Generics実践で「汎用Repository」を設計する
  2. Repositoryとは何かをざっくりイメージする
    1. 「データを扱うための共通の入り口」
  3. Step 1:Generic Type / Generic Interfaceの基本形を押さえる
    1. Generic Typeの例
    2. Generic Interfaceの例
  4. Step 2:汎用Repositoryインターフェースを設計する
    1. 「idを持つデータ」を扱う前提を決める
    2. Generic InterfaceとしてRepositoryを定義する
  5. Step 3:汎用Repositoryの実装クラスを作る
    1. Generics付きクラスで実装する
  6. Step 4:UserRepository / ProductRepository / OrderRepository を作る
    1. UserRepository
    2. ProductRepository
    3. OrderRepository
  7. Step 5:Genericsと配列の関係を意識してみる
  8. Day 23 練習テンプレート:汎用Repository+3種類のRepository
  9. Day 23のまとめ
  10. Day 23:異なるデータ型を「同じ仕組み」で管理するGenerics実践
  11. 異なるデータ型を「同じ仕組み」で扱うとはどういうことか
    1. まずは世界観をイメージする
  12. Step 1:共通の前提「idを持つデータ」を決める
    1. 「IDで管理する」というルールを型に落とし込む
  13. Step 2:Generic Interfaceで「共通の操作」を定義する
    1. 異なる型でも同じ操作を持つ「窓口」を作る
  14. Step 3:Genericsと配列で「実際に動く仕組み」を作る
    1. メモリ上でデータを管理するクラスを作る
  15. Step 4:異なるデータ型を同じ仕組みで管理してみる
    1. Userを管理する
    2. Productを管理する
    3. Orderを管理する
  16. Step 5:Generic Typeで「別の表現」も試してみる
    1. ManagerをType Aliasで表現するパターン
  17. Day 23 練習テンプレート:異なるデータ型を同じ仕組みで管理する
  18. Day 23のまとめ

Day 23:Generics実践で「汎用Repository」を設計する

第4週もいよいよ実践フェーズに入ってきました。 Day 23のテーマは Generics実践:汎用Repositoryを作る です。

ここで扱うキーワードは次の4つです。

  • Generic Type
  • Generic Interface
  • T extends
  • Genericsと配列

そして、実践テーマはこれです。

汎用Repositoryを作り、 UserRepository / ProductRepository / OrderRepository をGenericsで表現する。

「Repository」という言葉は少しカタめですが、 イメージとしては 「データを保存・検索・削除するための窓口」 だと思ってもらえると読みやすいです。

Repositoryとは何かをざっくりイメージする

「データを扱うための共通の入り口」

アプリケーションでは、ユーザー・商品・注文など、いろいろな種類のデータを扱います。 それぞれに対して、

  • 登録する
  • 取得する
  • 削除する

といった操作を行います。

この「データ操作」をひとまとめにしたものが、よく言われる Repository(リポジトリ) です。

今回のゴールは、

「どんな型のデータでも扱える汎用Repository」をGenericsで作る

ことです。

Step 1:Generic Type / Generic Interfaceの基本形を押さえる

Generic Typeの例

まずは、型にGenericsを使う例から見てみます。

// T型の配列をラップするGeneric Type
type List<T> = {
  items: T[];
};
TypeScript
  • List<T> は「T型の配列を持つ型」です。
  • List<User> や List<Product> のように、 Tに具体的な型を差し込んで使います。

Generic Interfaceの例

次に、インターフェースにGenericsを使う例です。

// T型の値を保存・取得するためのインターフェース
interface Storage<T> {
  save(item: T): void;
  getAll(): T[];
}
TypeScript
  • Storage<T> は「T型の値を扱うストレージのインターフェース」です。
  • Storage<User> や Storage<Product> のように使うことで、 「User専用のストレージ」「Product専用のストレージ」を表現できます。

今回のRepositoryも、この Generic Interface の形で作っていきます。

Step 2:汎用Repositoryインターフェースを設計する

「idを持つデータ」を扱う前提を決める

Repositoryでは、よく「IDで検索・削除」を行います。 そのため、ここでは「idプロパティを持つ型」を扱う前提にします。

// idプロパティを持つ型のための制約
type WithId = {
  id: number;
};
TypeScript

この WithId を使って、Genericsに制約をつけていきます。

Generic InterfaceとしてRepositoryを定義する

// 汎用Repositoryインターフェース
interface Repository<T extends WithId> {
  // データを追加する
  add(item: T): void;

  // IDで1件取得する
  findById(id: number): T | undefined;

  // すべてのデータを取得する
  findAll(): T[];

  // IDで削除する
  deleteById(id: number): boolean;
}
TypeScript

重要ポイント(ここをしっかり深掘りしておきたいところです):

  • T extends WithId
    • 「Tは必ず id: number を持っている型ですよ」という制約です。
    • これにより、item.id や u.id を安全に使えるようになります。
  • Repository<T> は、「T型のデータを扱うRepository」という意味になります。
    • Repository<User>
    • Repository<Product>
    • Repository<Order> など、Tに応じて「何を扱うRepositoryか」が変わります。

Step 3:汎用Repositoryの実装クラスを作る

Generics付きクラスで実装する

インターフェースだけでは動かないので、 実際に動く「メモリ上のRepository」をクラスで作ってみます。

// 汎用Repositoryの実装クラス
class InMemoryRepository<T extends WithId> implements Repository<T> {
  // データを配列で保持する(簡易的な「DB」のイメージ)
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  findById(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

  findAll(): T[] {
    // 外から配列をいじられないようにコピーを返す
    return [...this.items];
  }

  deleteById(id: number): boolean {
    const beforeLength = this.items.length;
    this.items = this.items.filter((item) => item.id !== id);
    const afterLength = this.items.length;
    return beforeLength !== afterLength;
  }
}
TypeScript

深掘りポイント:

  • class InMemoryRepository<T extends WithId>
    • クラス自体がGenericsを持っています。
    • このクラスは「T型のデータを扱うRepository」です。
  • implements Repository<T>
    • 「Repositoryインターフェースの契約を守ります」という宣言です。
    • メソッドのシグネチャ(引数・戻り値の型)がインターフェースと一致しているか、コンパイル時にチェックされます。
  • private items: T[] = [];
    • 「T型の配列」を内部に持つことで、 どんな型のデータでも扱える汎用Repositoryになっています。

Step 4:UserRepository / ProductRepository / OrderRepository を作る

ここからは、具体的な型を使って、 「User用」「Product用」「Order用」のRepositoryを作っていきます。

UserRepository

// User型の定義
type User = {
  id: number;
  name: string;
  email: string;
};

// User用のRepository
const userRepository = new InMemoryRepository<User>();

// 使用例
userRepository.add({ id: 1, name: "Taro", email: "[email protected]" });
userRepository.add({ id: 2, name: "Hanako", email: "[email protected]" });

console.log("すべてのユーザー:", userRepository.findAll());
console.log("ID=1のユーザー:", userRepository.findById(1));

userRepository.deleteById(2);
console.log("削除後のユーザー一覧:", userRepository.findAll());
TypeScript

ProductRepository

// Product型の定義
type Product = {
  id: number;
  title: string;
  price: number;
};

// Product用のRepository
const productRepository = new InMemoryRepository<Product>();

// 使用例
productRepository.add({ id: 10, title: "Book", price: 1200 });
productRepository.add({ id: 11, title: "Pen", price: 200 });

console.log("すべての商品:", productRepository.findAll());
console.log("ID=10の商品:", productRepository.findById(10));

productRepository.deleteById(11);
console.log("削除後の商品一覧:", productRepository.findAll());
TypeScript

OrderRepository

// Order型の定義
type Order = {
  id: number;
  userId: number;
  productId: number;
  quantity: number;
};

// Order用のRepository
const orderRepository = new InMemoryRepository<Order>();

// 使用例
orderRepository.add({ id: 100, userId: 1, productId: 10, quantity: 2 });
orderRepository.add({ id: 101, userId: 1, productId: 11, quantity: 1 });

console.log("すべての注文:", orderRepository.findAll());
console.log("ID=100の注文:", orderRepository.findById(100));

orderRepository.deleteById(101);
console.log("削除後の注文一覧:", orderRepository.findAll());
TypeScript

ここがGenericsの気持ちよさ:

  • InMemoryRepository<User>
  • InMemoryRepository<Product>
  • InMemoryRepository<Order>

と、同じクラスを「型だけ変えて」使い回せています。

ロジックは1つなのに、 扱うデータの型に応じて「型安全なRepository」が自動的にできあがる、 これがGenericsの大きな魅力です。

Step 5:Genericsと配列の関係を意識してみる

今回のRepositoryでは、内部で T[] を使っています。

private items: T[] = [];
TypeScript

ここには、Genericsと配列の関係がぎゅっと詰まっています。

  • T[] は「T型の配列」
  • findAll(): T[] は「T型の配列を返す」
  • add(item: T) は「T型の値を配列に追加する」

つまり、

「配列の要素の型」をTとして扱うことで、 どんな型の配列でも安全に扱えるようになっている

ということです。

Day 22で作った getFirst<T>() や reverse<T>() と同じ発想が、 Repositoryの中でも生きています。

Day 23 練習テンプレート:汎用Repository+3種類のRepository

最後に、今日の内容をひとつのファイルにまとめた練習テンプレートを載せます。 このままコピーして、フィールドを増やしたり、 メソッド(検索条件)を追加したりしながら、 「Genericsで汎用的な仕組みを作る」感覚を育ててみてください。

// day23-generics-repository.ts
// Day 23:Generics実践「汎用Repositoryを作る」

// idを持つ型の制約
type WithId = {
  id: number;
};

// 汎用Repositoryインターフェース
interface Repository<T extends WithId> {
  add(item: T): void;
  findById(id: number): T | undefined;
  findAll(): T[];
  deleteById(id: number): boolean;
}

// 汎用Repositoryの実装クラス(メモリ上)
class InMemoryRepository<T extends WithId> implements Repository<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  findById(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

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

  deleteById(id: number): boolean {
    const beforeLength = this.items.length;
    this.items = this.items.filter((item) => item.id !== id);
    const afterLength = this.items.length;
    return beforeLength !== afterLength;
  }
}

// ===== UserRepository =====
type User = {
  id: number;
  name: string;
  email: string;
};

const userRepository = new InMemoryRepository<User>();

userRepository.add({ id: 1, name: "Taro", email: "[email protected]" });
userRepository.add({ id: 2, name: "Hanako", email: "[email protected]" });

console.log("すべてのユーザー:", userRepository.findAll());
console.log("ID=1のユーザー:", userRepository.findById(1));

userRepository.deleteById(2);
console.log("削除後のユーザー一覧:", userRepository.findAll());

// ===== ProductRepository =====
type Product = {
  id: number;
  title: string;
  price: number;
};

const productRepository = new InMemoryRepository<Product>();

productRepository.add({ id: 10, title: "Book", price: 1200 });
productRepository.add({ id: 11, title: "Pen", price: 200 });

console.log("すべての商品:", productRepository.findAll());
console.log("ID=10の商品:", productRepository.findById(10));

productRepository.deleteById(11);
console.log("削除後の商品一覧:", productRepository.findAll());

// ===== OrderRepository =====
type Order = {
  id: number;
  userId: number;
  productId: number;
  quantity: number;
};

const orderRepository = new InMemoryRepository<Order>();

orderRepository.add({ id: 100, userId: 1, productId: 10, quantity: 2 });
orderRepository.add({ id: 101, userId: 1, productId: 11, quantity: 1 });

console.log("すべての注文:", orderRepository.findAll());
console.log("ID=100の注文:", orderRepository.findById(100));

orderRepository.deleteById(101);
console.log("削除後の注文一覧:", orderRepository.findAll());
TypeScript

Day 23のまとめ

Day 23では、

  • Generic Type / Generic Interfaceの基本形
  • T extends で「扱う型に制約をつける」考え方
  • Genericsと配列(T[])の関係
  • 汎用Repositoryを作り、User / Product / OrderのRepositoryを 同じクラスから型だけ変えて生成する流れ

を、具体的なコードとともに体験しました。

Genericsは、「一度作った仕組みを、いろいろな型に対して安全に使い回す」ための強力な道具です。 今日のRepositoryをベースに、

  • findByUserId<T>()
  • findByStatus<T>()

のようなメソッドを追加していくと、 「型で守られたミニ・データレイヤー」を自分の手で育てていく感覚が味わえるはずです。


Day 23:異なるデータ型を「同じ仕組み」で管理するGenerics実践

第4週のDay 23では、Genericsを使って 「異なるデータ型を同じ仕組みで管理する」 ことにチャレンジしていきます。 テーマはシンプルですが、とても実務に近い内容です。

UserでもProductでもOrderでも、 同じ「管理の仕組み」で扱えるようにする。

そのために使うキーワードは次の4つです。

  • Generic Type
  • Generic Interface
  • T extends
  • Genericsと配列

ここから、ステップバイステップで「型の違いを吸収する仕組み」を作っていきます。

異なるデータ型を「同じ仕組み」で扱うとはどういうことか

まずは世界観をイメージする

アプリケーションには、いろいろな種類のデータが登場します。

  • ユーザー(User)
  • 商品(Product)
  • 注文(Order)
  • それ以外にも、コメント・タグ・通知…などなど

それぞれの型は違いますが、やりたいことは似ています。

  • 登録する
  • 一覧を取得する
  • IDで検索する
  • 削除する

この「共通の操作」を、ひとつの仕組みでまとめてしまおうというのが、今回のゴールです。

Step 1:共通の前提「idを持つデータ」を決める

「IDで管理する」というルールを型に落とし込む

多くのデータは「ID」で管理されます。 そこで、まずは「idプロパティを持つ型」という前提を型で表現します。

// idプロパティを持つ型のための制約
type WithId = {
  id: number;
};
TypeScript

この WithId を使って、Genericsに「Tは必ずidを持っている型ですよ」という制約をつけていきます。

Step 2:Generic Interfaceで「共通の操作」を定義する

異なる型でも同じ操作を持つ「窓口」を作る

ここでは、「データを管理するためのインターフェース」をGenericsで定義します。

// T型のデータを管理するためのGeneric Interface
interface Manager<T extends WithId> {
  // データを追加する
  add(item: T): void;

  // すべてのデータを取得する
  getAll(): T[];

  // IDで1件取得する
  getById(id: number): T | undefined;

  // IDで削除する
  removeById(id: number): boolean;
}
TypeScript

重要ポイント(ここはしっかり押さえておきたいところです):

  • T extends WithId
    • 「Tは必ず id: number を持っている型ですよ」という制約です。
    • これにより、item.id や data.id を安全に使えます。
  • Manager<T> は、「T型のデータを管理する仕組み」という意味になります。
    • Manager<User>
    • Manager<Product>
    • Manager<Order> といった具合に、Tに応じて「何を管理するか」が変わります。

Step 3:Genericsと配列で「実際に動く仕組み」を作る

メモリ上でデータを管理するクラスを作る

インターフェースだけでは動かないので、 実際に動く「メモリ上の管理クラス」を作ってみます。

// 汎用的なデータ管理クラス(メモリ上)
class InMemoryManager<T extends WithId> implements Manager<T> {
  // T型の配列としてデータを保持する
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    // 外から直接いじられないようにコピーを返す
    return [...this.items];
  }

  getById(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

  removeById(id: number): boolean {
    const beforeLength = this.items.length;
    this.items = this.items.filter((item) => item.id !== id);
    const afterLength = this.items.length;
    return beforeLength !== afterLength;
  }
}
TypeScript

深掘りポイント:

  • private items: T[] = [];
    • 「T型の配列」を内部に持つことで、 どんな型のデータでも同じ仕組みで管理できるようになっています。
  • getAll(): T[]
    • 戻り値も T[] なので、呼び出し側は「このManagerが何を扱っているか」に応じて、 ちゃんと型安全にデータを扱えます。
  • getById(id: number): T | undefined
    • 「見つからない可能性」を undefined として型に含めているのもポイントです。

Step 4:異なるデータ型を同じ仕組みで管理してみる

ここからは、具体的な型を使って、 「User」「Product」「Order」を同じ InMemoryManager で管理してみます。

Userを管理する

// User型の定義
type User = {
  id: number;
  name: string;
  email: string;
};

// User用のManager
const userManager = new InMemoryManager<User>();

// データ追加
userManager.add({ id: 1, name: "Taro", email: "[email protected]" });
userManager.add({ id: 2, name: "Hanako", email: "[email protected]" });

// 一覧取得
console.log("User一覧:", userManager.getAll());

// IDで取得
console.log("ID=1のUser:", userManager.getById(1));

// 削除
userManager.removeById(2);
console.log("削除後のUser一覧:", userManager.getAll());
TypeScript

Productを管理する

// Product型の定義
type Product = {
  id: number;
  title: string;
  price: number;
};

// Product用のManager
const productManager = new InMemoryManager<Product>();

productManager.add({ id: 10, title: "Book", price: 1200 });
productManager.add({ id: 11, title: "Pen", price: 200 });

console.log("Product一覧:", productManager.getAll());
console.log("ID=10のProduct:", productManager.getById(10));

productManager.removeById(11);
console.log("削除後のProduct一覧:", productManager.getAll());
TypeScript

Orderを管理する

// Order型の定義
type Order = {
  id: number;
  userId: number;
  productId: number;
  quantity: number;
};

// Order用のManager
const orderManager = new InMemoryManager<Order>();

orderManager.add({ id: 100, userId: 1, productId: 10, quantity: 2 });
orderManager.add({ id: 101, userId: 1, productId: 11, quantity: 1 });

console.log("Order一覧:", orderManager.getAll());
console.log("ID=100のOrder:", orderManager.getById(100));

orderManager.removeById(101);
console.log("削除後のOrder一覧:", orderManager.getAll());
TypeScript

ここが「異なるデータ型を同じ仕組みで管理する」おいしいところです:

  • InMemoryManager<User>
  • InMemoryManager<Product>
  • InMemoryManager<Order>

と、同じクラスを「型だけ変えて」使い回せています。

ロジックは1つなのに、 扱うデータの型に応じて「型安全な管理クラス」が自動的にできあがる。 これが、Generics+配列+T extends の組み合わせの強さです。

Step 5:Generic Typeで「別の表現」も試してみる

ManagerをType Aliasで表現するパターン

インターフェースではなく、Type Aliasで書くこともできます。

// Generic TypeとしてのManager
type ManagerType<T extends WithId> = {
  add(item: T): void;
  getAll(): T[];
  getById(id: number): T | undefined;
  removeById(id: number): boolean;
};
TypeScript

この ManagerType<T> を使って、 関数ベースの管理仕組みを作ることもできます。

// 関数ベースのManagerを作るヘルパー
function createManager<T extends WithId>(): ManagerType<T> {
  let items: T[] = [];

  return {
    add(item: T) {
      items.push(item);
    },
    getAll() {
      return [...items];
    },
    getById(id: number) {
      return items.find((item) => item.id === id);
    },
    removeById(id: number) {
      const beforeLength = items.length;
      items = items.filter((item) => item.id !== id);
      const afterLength = items.length;
      return beforeLength !== afterLength;
    },
  };
}
TypeScript

ポイント:

  • Generic InterfaceでもGeneric Typeでも、 「Tを使って異なる型を同じ仕組みで扱う」という発想は共通です。
  • クラスが好きな方は InMemoryManager<T>、 関数スタイルが好きな方は createManager<T>() と、 自分の好みに合わせて選べます。

Day 23 練習テンプレート:異なるデータ型を同じ仕組みで管理する

最後に、今日の内容をひとつのファイルにまとめたテンプレートを載せます。 このままコピーして、型を増やしたり、 メソッド(検索条件)を追加したりしながら、 「Genericsで共通の仕組みを作る」感覚を育ててみてください。

// day23-generics-manage-multiple-types.ts
// Day 23:異なるデータ型を同じ仕組みで管理する

// idを持つ型の制約
type WithId = {
  id: number;
};

// Generic InterfaceとしてのManager
interface Manager<T extends WithId> {
  add(item: T): void;
  getAll(): T[];
  getById(id: number): T | undefined;
  removeById(id: number): boolean;
}

// クラスベースの汎用Manager
class InMemoryManager<T extends WithId> implements Manager<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return [...this.items];
  }

  getById(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }

  removeById(id: number): boolean {
    const beforeLength = this.items.length;
    this.items = this.items.filter((item) => item.id !== id);
    const afterLength = this.items.length;
    return beforeLength !== afterLength;
  }
}

// ===== User =====
type User = {
  id: number;
  name: string;
  email: string;
};

const userManager = new InMemoryManager<User>();

userManager.add({ id: 1, name: "Taro", email: "[email protected]" });
userManager.add({ id: 2, name: "Hanako", email: "[email protected]" });

console.log("User一覧:", userManager.getAll());
console.log("ID=1のUser:", userManager.getById(1));

userManager.removeById(2);
console.log("削除後のUser一覧:", userManager.getAll());

// ===== Product =====
type Product = {
  id: number;
  title: string;
  price: number;
};

const productManager = new InMemoryManager<Product>();

productManager.add({ id: 10, title: "Book", price: 1200 });
productManager.add({ id: 11, title: "Pen", price: 200 });

console.log("Product一覧:", productManager.getAll());
console.log("ID=10のProduct:", productManager.getById(10));

productManager.removeById(11);
console.log("削除後のProduct一覧:", productManager.getAll());

// ===== Order =====
type Order = {
  id: number;
  userId: number;
  productId: number;
  quantity: number;
};

const orderManager = new InMemoryManager<Order>();

orderManager.add({ id: 100, userId: 1, productId: 10, quantity: 2 });
orderManager.add({ id: 101, userId: 1, productId: 11, quantity: 1 });

console.log("Order一覧:", orderManager.getAll());
console.log("ID=100のOrder:", orderManager.getById(100));

orderManager.removeById(101);
console.log("削除後のOrder一覧:", orderManager.getAll());
TypeScript

Day 23のまとめ

Day 23では、

  • WithId で「idを持つ型」という共通前提を決めること
  • Generic Interface(Manager<T>)で「共通の操作」を型として定義すること
  • T extends で「扱う型に制約をつける」考え方
  • Genericsと配列(T[])を組み合わせて、 異なるデータ型を同じ仕組みで管理する流れ

を、具体的なコードとコメント付きで体験しました。

Genericsは、「一度作った仕組みを、いろいろな型に対して安全に使い回す」ための道具です。 今日のManagerをベースに、

  • findByUserId<T>()
  • filterByPrice<T>()

のようなメソッドを追加していくと、 「型で守られたミニ・データ管理レイヤー」を自分の手で育てていく感覚が味わえるはずです。

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