- Day 23:Generics実践で「汎用Repository」を設計する
- Repositoryとは何かをざっくりイメージする
- Step 1:Generic Type / Generic Interfaceの基本形を押さえる
- Step 2:汎用Repositoryインターフェースを設計する
- Step 3:汎用Repositoryの実装クラスを作る
- Step 4:UserRepository / ProductRepository / OrderRepository を作る
- Step 5:Genericsと配列の関係を意識してみる
- Day 23 練習テンプレート:汎用Repository+3種類のRepository
- Day 23のまとめ
- Day 23:異なるデータ型を「同じ仕組み」で管理するGenerics実践
- 異なるデータ型を「同じ仕組み」で扱うとはどういうことか
- Step 1:共通の前提「idを持つデータ」を決める
- Step 2:Generic Interfaceで「共通の操作」を定義する
- Step 3:Genericsと配列で「実際に動く仕組み」を作る
- Step 4:異なるデータ型を同じ仕組みで管理してみる
- Step 5:Generic Typeで「別の表現」も試してみる
- Day 23 練習テンプレート:異なるデータ型を同じ仕組みで管理する
- 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[];
};
TypeScriptList<T>は「T型の配列を持つ型」です。List<User>やList<Product>のように、 Tに具体的な型を差し込んで使います。
Generic Interfaceの例
次に、インターフェースにGenericsを使う例です。
// T型の値を保存・取得するためのインターフェース
interface Storage<T> {
save(item: T): void;
getAll(): T[];
}
TypeScriptStorage<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を安全に使えるようになります。
- 「Tは必ず
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());
TypeScriptProductRepository
// 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());
TypeScriptOrderRepository
// 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());
TypeScriptDay 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を安全に使えます。
- 「Tは必ず
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());
TypeScriptProductを管理する
// 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());
TypeScriptOrderを管理する
// 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());
TypeScriptDay 23のまとめ
Day 23では、
WithIdで「idを持つ型」という共通前提を決めること- Generic Interface(
Manager<T>)で「共通の操作」を型として定義すること T extendsで「扱う型に制約をつける」考え方- Genericsと配列(
T[])を組み合わせて、 異なるデータ型を同じ仕組みで管理する流れ
を、具体的なコードとコメント付きで体験しました。
Genericsは、「一度作った仕組みを、いろいろな型に対して安全に使い回す」ための道具です。 今日のManagerをベースに、
findByUserId<T>()filterByPrice<T>()
のようなメソッドを追加していくと、 「型で守られたミニ・データ管理レイヤー」を自分の手で育てていく感覚が味わえるはずです。
