TypeScript | 関数・クラス・ジェネリクス:ジェネリクス基礎 - interfaceのジェネリクス

TypeScript TypeScript
スポンサーリンク

ゴール:「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(); // コンパイルは通る(実行時に落ちるかも)
TypeScript

Box<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[]
TypeScript

interface 側で「操作の形」を決め、
クラス側で「具体的な実装」を書く。

その橋渡しにジェネリクスが入ることで、
「どの型を扱う実装なのか」が明確になります。

典型パターン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

型注釈がないので、valueany になってしまいます。

「中身の型だけ変えたい」と思ったら、
まず 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

そして、numberstring・自作の型を T に入れて型推論を眺めてみてください。

そこで、

「interface は“型のテンプレート”で、ジェネリクスはその“穴”なんだ」

と感じられたら、
interface のジェネリクスの基礎はもうしっかり掴めています。

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