- ゴール:「interface は“型のテンプレート”であり、<T> で“穴あきテンプレート”にできると理解する
- 基本形:interface に <T> を付けるだけ
- 典型パターン1:API レスポンスをジェネリクス interface で表す
- 典型パターン2:リポジトリ・ストアの interface をジェネリクスにする
- 典型パターン3:イベント・メッセージのペイロードをジェネリクスにする
- 制約付きジェネリクス interface:「T は最低限これを持つ」
- interface ジェネリクスと関数の組み合わせ
- ありがちなつまずきポイントとコツ
- まとめ:interface のジェネリクスを自分の言葉で説明すると
ゴール:「interface は“型のテンプレート”であり、<T> で“穴あきテンプレート”にできると理解する
クラスのジェネリクスが「インスタンス全体のルール」を決めるものだとしたら、
interface のジェネリクスは「型の設計図に穴を開ける仕組み」です。
一度つかめば、
「同じ形だけど、中身の型だけ違うもの」
をきれいに表現できるようになります。
API レスポンス、リポジトリ、イベント、結果型など、実務で頻出のパターンが一気に整理されます。
基本形:interface に <T> を付けるだけ
「中身の型だけ変えたい箱」を interface で表現する
まずは一番シンプルな例から。
interface Box<T> {
value: T;
}
const nBox: Box<number> = { value: 123 };
const sBox: Box<string> = { value: "hello" };
const uBox: Box<{ id: number }> = { value: { id: 1 } };
TypeScriptここでやっていることはとても単純です。
interface Box<T> が「value というプロパティを 1 つ持つ“型のテンプレート”」
T が「その value の中身の型の穴」
という関係になっています。
重要なのは、
同じ Box という“形”を保ったまま
中身の型だけを T で差し替えられる
という点です。
Box<number> も Box<string> も、
「value というプロパティを 1 つ持つ」という構造は同じですが、
中身の型だけが違います。
any との違いを一度はっきり見る
any で書くとこうなります。
interface BoxAny {
value: any;
}
const b: BoxAny = { value: 123 };
b.value.toUpperCase(); // コンパイルは通る(実行時に落ちるかも)
TypeScriptBox<number> なら value は必ず number、Box<string> なら必ず string です。
「形は共通、でも中身の型はきっちり守る」
これが interface ジェネリクスの基本的な価値です。
典型パターン1:API レスポンスをジェネリクス interface で表す
成功時の data だけをジェネリクスにする
API のレスポンスは、ジェネリクス interface のド定番です。
interface ApiResponse<T> {
success: boolean;
data: T | null;
errorMessage?: string;
}
TypeScript使うときはこうなります。
interface User {
id: number;
name: string;
}
const userRes: ApiResponse<User> = {
success: true,
data: { id: 1, name: "Taro" },
};
const countRes: ApiResponse<number> = {
success: true,
data: 10,
};
TypeScriptここでのポイントは、
ApiResponse という“レスポンスの形”は固定
中身の data の型だけを T で差し替える
という設計になっていることです。
セキュリティ視点で見ると、
「レスポンスの構造は統一しておきたい(success, errorMessage など)」
「ただし、返すデータの型はエンドポイントごとに違う」
という現実を、型で綺麗に表現できています。
union と組み合わせた「成功 or 失敗」パターン
もう少し厳密にするなら、成功と失敗を union に分けることもできます。
interface ApiSuccess<T> {
success: true;
data: T;
}
interface ApiFailure {
success: false;
errorMessage: string;
}
type ApiResult<T> = ApiSuccess<T> | ApiFailure;
TypeScript使い方は同じです。
const ok: ApiResult<User> = {
success: true,
data: { id: 1, name: "Taro" },
};
const ng: ApiResult<User> = {
success: false,
errorMessage: "Unauthorized",
};
TypeScriptここでも、「ジェネリクスは“成功時の中身の型”だけを可変にする」という役割です。
典型パターン2:リポジトリ・ストアの interface をジェネリクスにする
「何を保存するか」だけをジェネリクスにする
データを保存・取得するリポジトリも、ジェネリクス interface と相性抜群です。
interface Repository<T> {
add(item: T): void;
getAll(): T[];
findById(id: number): T | undefined;
}
TypeScriptこれを具体的な型に適用します。
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
price: number;
}
const userRepo: Repository<User> = /* 実装 */;
const productRepo: Repository<Product> = /* 実装 */;
TypeScriptここでのポイントは、
Repository という「操作のセット」は共通
T だけを変えることで「何を扱うリポジトリか」が変わる
という構造です。
セキュリティ的には、
「User 用のリポジトリには User しか入らない」
「Product 用のリポジトリには Product しか入らない」
という制約を、型で保証できます。
実装クラス側で T を固定する
ジェネリクス interface を、クラスで実装するとこうなります。
class InMemoryRepository<T> implements Repository<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
findById(id: number): T | undefined {
return this.items.find((item: any) => item.id === id);
}
}
TypeScript使うときはこうです。
const userRepo2 = new InMemoryRepository<User>();
userRepo2.add({ id: 1, name: "Taro" });
const allUsers = userRepo2.getAll(); // User[]
TypeScriptinterface 側で「操作の形」を決め、
クラス側で「具体的な実装」を書く。
その橋渡しにジェネリクスが入ることで、
「どの型を扱う実装なのか」が明確になります。
典型パターン3:イベント・メッセージのペイロードをジェネリクスにする
「イベントの形は同じ、中身だけ変わる」パターン
イベントやメッセージも、ジェネリクス interface のおいしいところです。
interface Event<TPayload> {
type: string;
payload: TPayload;
timestamp: number;
}
TypeScript使うときはこうなります。
interface LoginPayload {
userId: number;
ip: string;
}
interface LogoutPayload {
userId: number;
}
const loginEvent: Event<LoginPayload> = {
type: "login",
payload: { userId: 1, ip: "127.0.0.1" },
timestamp: Date.now(),
};
const logoutEvent: Event<LogoutPayload> = {
type: "logout",
payload: { userId: 1 },
timestamp: Date.now(),
};
TypeScriptここでの構造は、
Event という“枠”(type, payload, timestamp)は共通
payload の中身だけを TPayload で差し替える
というものです。
セキュリティ的には、
「ログ・監査イベントのフォーマットは統一したい」
「でも、イベントごとに持つ情報は違う」
という現場の要件にぴったりハマります。
制約付きジェネリクス interface:「T は最低限これを持つ」
「id を持つものだけ扱う interface」
ジェネリクス interface にも extends で制約をつけられます。
interface HasId {
id: number;
}
interface IdMap<T extends HasId> {
[id: number]: T;
}
TypeScript使うときはこうです。
interface User {
id: number;
name: string;
}
interface NoId {
name: string;
}
const users: IdMap<User> = {
1: { id: 1, name: "Taro" },
};
// const invalid: IdMap<NoId> = { 1: { name: "x" } }; // エラー:id がない
TypeScriptここでのポイントは、
T extends HasId によって「T は必ず id を持つ」
IdMap<T> は「id をキーに T を引けるマップ」
という設計になっていることです。
セキュリティ的には、
「ID をキーにアクセス制御や監査をしたいので、
ID を持たないものはそもそもこの構造に入れさせない」
という“型レベルのガード”になっています。
interface ジェネリクスと関数の組み合わせ
「ジェネリクス interface を受け取る関数」
ジェネリクス interface を引数や戻り値に使うと、
コード全体の一貫性がぐっと上がります。
interface ApiResponse<T> {
success: boolean;
data: T | null;
}
function handleResponse<T>(res: ApiResponse<T>): T | null {
if (!res.success) {
return null;
}
return res.data;
}
TypeScript使うときはこうです。
const userRes: ApiResponse<User> = /* ... */;
const user = handleResponse(userRes); // User | null
const countRes: ApiResponse<number> = /* ... */;
const count = handleResponse(countRes); // number | null
TypeScriptここでのポイントは、
ApiResponse<T> という“型のテンプレート”
handleResponse<T> という“処理のテンプレート”
が、T を共有していることです。
「データの形」と「それを扱う関数」の両方をジェネリクスで揃えると、
型の整合性が崩れにくくなります。
ありがちなつまずきポイントとコツ
「interface に <T> を付け忘れて、全部 any になる」
よくあるのがこれです。
interface BoxBad {
value;
}
TypeScript型注釈がないので、value は any になってしまいます。
「中身の型だけ変えたい」と思ったら、
まず interface 名の後ろに <T> を付ける癖をつけてください。
interface BoxGood<T> {
value: T;
}
TypeScript「T をどこに置くか分からなくなる」
T を使う場所は、基本的に次の 2 つです。
プロパティの型
メソッドの引数・戻り値の型
例えば:
interface Cache<T> {
get(key: string): T | undefined;
set(key: string, value: T): void;
}
TypeScript「この interface は“何の Cache なのか”」を T で表現し、
その T をプロパティやメソッドに反映させる、という流れです。
まとめ:interface のジェネリクスを自分の言葉で説明すると
最後に、あなた自身の言葉でこう整理してみてください。
interface のジェネリクスは、
「interface 名の後ろに <T> を付けて、
その T をプロパティやメソッドの型に使うことで、
“形は同じだけど中身の型だけ違う”設計図を作る仕組み」。
ApiResponse<T>・Repository<T>・Event<TPayload> のように、
形(フィールド構造)は共通
中身の型だけを T で差し替える
ことで、
柔軟だけど、型の境界はきっちり守られた設計になる。
まずは次の 3 つを、自分の手で書いてみてください。
interface Box<T> { value: T; }
interface ApiResponse<T> { success: boolean; data: T | null; }
interface Repository<T> { add(item: T): void; getAll(): T[]; }
TypeScriptそして、number・string・自作の型を T に入れて型推論を眺めてみてください。
そこで、
「interface は“型のテンプレート”で、ジェネリクスはその“穴”なんだ」
と感じられたら、
interface のジェネリクスの基礎はもうしっかり掴めています。
