ReDevLab
TypeScript 24分で読める

TypeScriptジェネリクス実践ガイド|基本構文から高度な型パターンまで

TypeScriptジェネリクスの基本構文・型制約・Reactでの実践例・Conditional Typesなど高度なパターンまで、コード例付きで体系的に解説します。

編集部
TypeScriptジェネリクス実践ガイド|基本構文から高度な型パターンまで

TypeScriptのジェネリクスは、 型安全性とコードの再利用性を両立する実践的な武器です。 この実践ガイドでは、基本構文から高度な型パターンまでを段階的に扱います。 たとえばany型で書いていた汎用関数を、 ジェネリクスに置き換えるだけで補完とエラー検出が劇的に改善します。 本記事を読み終えるころには、Reactコンポーネントへの適用や Conditional Types(条件付き型)の設計まで、 実務で使えるジェネリクスの引き出しが一通りそろいます。

前提条件と環境構築

この記事のコード例はすべてTypeScript 5.8以降で動作確認しています。 まず開発環境を整えましょう。

必要な環境(Node.js・TypeScript 5.8以降)

TypeScript 5.8はNode.jsのコンパイルキャッシュAPIを活用し、 ビルド時間が従来の2〜3倍高速になりました (TypeScript 5.8リリースノート)。 ジェネリクスの学習にも最新版の利用をおすすめします。

動作確認済みの環境は以下のとおりです。

  • Node.js 20.x 以降
  • TypeScript 5.8 以降
  • エディタ:VS Code(TypeScript拡張機能付き)

1. TypeScriptのインストール

プロジェクトにTypeScriptを導入します。 グローバルではなくローカルインストールが推奨です。

# macOS / Linux
mkdir ts-generics-practice && cd ts-generics-practice
npm init -y
npm install -D typescript@latest
npx tsc --init
# Windows PowerShell
mkdir ts-generics-practice; cd ts-generics-practice
npm init -y
npm install -D typescript@latest
npx tsc --init

2. バージョンの確認

インストール後、バージョンが5.8以上か確認してください。

npx tsc --version

以下のように表示されれば準備完了です。

Version 5.8.3

3. tsconfig.jsonの確認ポイント

npx tsc --initで生成されるファイルのうち、 strictオプションがtrueになっていることを確認してください。 ジェネリクスの型チェックを正しく機能させるために必須の設定です。

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "NodeNext"
  }
}

前提知識(インターフェース・型エイリアス・ユニオン型)

ジェネリクスを理解するには、3つの型機能を押さえておく必要があります。 以下のコードが読めればこの記事を進められます。

// 型エイリアス:型に名前をつける機能
type UserID = string | number;

// インターフェース:オブジェクトの構造を定義する機能
interface User {
  id: UserID;
  name: string;
  email: string;
}

// ユニオン型:複数の型のいずれかを表す
type ApiResponse = User | null;

型エイリアス(type)はあらゆる型に別名をつけられます。 インターフェース(interface)はオブジェクトの形を定義するもの。 ユニオン型(|)は「AまたはB」を表す型です。

ジェネリクスはこれらの仕組みの上に成り立っています。 特にインターフェースとの組み合わせは頻出パターンなので、 interfaceの基本構文に不安がある方は サバイバルTypeScriptで 復習してから読み進めてください。

ジェネリクスの基本構文をステップバイステップで理解する

ジェネリクスは「型を変数のように扱う」仕組みです。 関数やクラスを定義する時点では型を決めず、使う側が型を指定できます。 ここでは基本構文を3ステップで押さえていきます。

any型の問題とジェネリクスが解決すること

型の安全性とコードの共通化を両立する手段、それがジェネリクスです(サバイバルTypeScript)。

any型でも汎用的な関数は書けます。しかし戻り値の型情報が失われ、バグの温床になります。

// any型:戻り値がanyになり型チェックが効かない
function getFirst(items: any[]): any {
  return items[0];
}
const val = getFirst([1, 2, 3]); // val は any型

// ジェネリクス:型情報が保持される
function getFirstTyped<T>(items: T[]): T {
  return items[0];
}
const num = getFirstTyped([1, 2, 3]); // num は number型

anyは型チェックを完全に放棄します。 一方ジェネリクスなら、呼び出し時に型が確定し、 補完やエラー検出が有効なままです。

関数・クラス・インターフェースでの宣言と型推論

ジェネリクスの<T>構文は関数・クラス・インターフェースの3箇所で使えます。

// 関数
function wrap<T>(value: T): { data: T } {
  return { data: value };
}

// インターフェース
interface ApiResponse<T> {
  status: number;
  body: T;
}

// クラス
class Stack<T> {
  private items: T[] = [];
  push(item: T): void { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

TypeScriptは引数から型を自動推論します。 wrap("hello")と書けばTstringに確定します。 推論が曖昧な場合はwrap<string>("hello")と明示してください。

複数の型パラメータとデフォルト型パラメータ

型パラメータは複数指定できます。キーと値の型が異なるマップ関数などで役立ちます。

// 複数の型パラメータ
function toEntry<K, V>(key: K, value: V): [K, V] {
  return [key, value];
}
const entry = toEntry("name", 42); // [string, number]

デフォルト型パラメータ(<T = DefaultType>構文)を使うと、 型指定を省略した際の既定値を設定できます (サバイバルTypeScript)。

// デフォルト型パラメータ
interface Payload<T = string> {
  data: T;
  timestamp: number;
}

const p1: Payload = { data: "hello", timestamp: Date.now() };
const p2: Payload<number> = { data: 100, timestamp: Date.now() };

デフォルト型を持つパラメータは、持たないパラメータの後に配置する必要があります。 <K, V = string>は有効ですが、<K = string, V>はコンパイルエラーになります。

型パラメータの制約とユーティリティ型の活用

ジェネリクスの真価は、型パラメータに制約を加えることで発揮されます。 制約なしの型パラメータはunknownと同様に扱われ、 プロパティへのアクセスでエラーになります (サバイバルTypeScript - 型引数の制約)。 ここでは制約の付け方と、組み込みユーティリティ型の実践的な活用法を紹介します。

extendsによる制約とkeyofの組み合わせ

extendsキーワードを使うと、型パラメータの範囲を絞れます。 さらにkeyofと組み合わせると、 オブジェクトのキーに対する型安全なアクセスが実現します。

// extendsでTにlengthプロパティを要求する
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

getLength("hello");    // OK: stringはlengthを持つ
getLength([1, 2, 3]);  // OK: 配列もlengthを持つ
// getLength(123);      // エラー: numberにlengthはない

// keyofでオブジェクトのキーを制約する
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "太郎", age: 30 };
getProperty(user, "name"); // 戻り値はstring型
// getProperty(user, "email"); // エラー: "email"はkeyof Tに含まれない

K extends keyof Tというパターンは頻出します。 存在しないキーへのアクセスをコンパイル時に防げるため、 ランタイムエラーの削減に直結します。

Partial・Pick・Omit・Recordの実践的な使い方

TypeScriptの組み込みユーティリティ型は、すべてジェネリクスで実装されています (TypeScript公式 - Utility Types)。 以下は実務でよく使う4つの型です。

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "member";
}

// Partial<T>: 全プロパティをオプショナルに(更新APIで便利)
function updateUser(id: number, patch: Partial<User>): void {
  // patch.nameだけ渡してもOK
}

// Pick<T, K>: 必要なプロパティだけ抽出
type UserSummary = Pick<User, "id" | "name">;
// => { id: number; name: string }

// Omit<T, K>: 特定プロパティを除外(作成時にidを除く等)
type CreateUserInput = Omit<User, "id">;
// => { name: string; email: string; role: "admin" | "member" }

// Record<K, T>: キーと値の型を指定した辞書型
const permissions: Record<User["role"], string[]> = {
  admin: ["read", "write", "delete"],
  member: ["read"],
};

カスタムユーティリティ型の作成

組み込み型だけで対応できない場面では、独自のユーティリティ型を作ります。 Conditional Types(条件付き型)とMapped Typesを組み合わせると、 柔軟な型変換が可能です (TypeScript公式 - Conditional Types)。

// 特定の型を持つプロパティだけを抽出する型
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

type StringFields = PickByType<User, string>;
// => { name: string; email: string }

// 全プロパティをreadonly かつ non-nullable にする型
type StrictReadonly<T> = {
  readonly [K in keyof T]-?: NonNullable<T[K]>;
};

[K in keyof T as ...]as句によるキー再マッピングは、 TypeScript 4.1で追加された機能です (TypeScript公式 - Mapped Types)。 条件に合わないキーをneverにすることで、フィルタリングを実現しています。

筆者の経験では、カスタムユーティリティ型は 3回以上同じパターンが出てきたタイミングで切り出すのがおすすめです。 早すぎる抽象化は可読性を下げるため、 まずは具体的なコードで書き始めてください。

実践例:Reactコンポーネントでのジェネリクス活用

Reactでジェネリクスを使うと、 データの型に依存しない汎用コンポーネントを型安全に構築できます。 ここでは、実務で頻出する2つのパターンと、 .tsxファイル特有の落とし穴を紹介します。

ジェネリックなデータフェッチコンポーネントの実装

APIのレスポンス型をジェネリクスで受け取る設計です。 戻り値の型でコンポーネントをパラメータ化します (Developer Way)。

// DataFetcher.tsx - TypeScript 5.8+対応
type DataFetcherProps<T> = {
  url: string;
  render: (data: T) => React.ReactNode;
};

function DataFetcher<T,>({ url, render }: DataFetcherProps<T>) {
  const [data, setData] = React.useState<T | null>(null);

  React.useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((json: T) => setData(json));
  }, [url]);

  if (!data) return <p>読み込み中...</p>;
  return <>{render(data)}</>;
}

// 使用例:型が自動推論されます
type User = { id: number; name: string };
<DataFetcher<User> url="/api/users/1" render={(user) => <p>{user.name}</p>} />;

呼び出し時に<User>を渡すだけで、renderの引数に型が伝播します。

型安全なテーブルコンポーネントの設計

テーブルの行データ型をジェネリクスで抽象化するパターンです。 keyof Tで列キーを制約し、 存在しないプロパティ名の指定をコンパイル時に防ぎます。

// GenericTable.tsx
type Column<T> = {
  key: keyof T;
  header: string;
};

type TableProps<T> = {
  items: T[];
  columns: Column<T>[];
};

function GenericTable<T,>({ items, columns }: TableProps<T>) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => (
            <th key={String(col.key)}>{col.header}</th>
          ))}
        </tr>
      </thead>
      <tbody>
        {items.map((item, i) => (
          <tr key={i}>
            {columns.map((col) => (
              <td key={String(col.key)}>{String(item[col.key])}</td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// 使用例:columnsのkeyに"age"等の誤ったキーを書くとコンパイルエラー
type Product = { name: string; price: number };
<GenericTable<Product>
  items={[{ name: "Widget", price: 500 }]}
  columns={[
    { key: "name", header: "商品名" },
    { key: "price", header: "価格" },
  ]}
/>;

key: "age"のようにProductに存在しないキーを指定すると、エディタ上で即座にエラーが表示されます。

.tsxファイル特有のJSX競合と<T,>構文による回避

.tsxファイルでジェネリック関数を定義すると、<T>がJSXの開きタグと解釈されてパースエラーになります(marsquai)。

// ❌ エラー:JSXタグとして解釈される
const identity = <T>(arg: T): T => arg;

// ✅ 方法1:末尾カンマで曖昧さを解消
const identity = <T,>(arg: T): T => arg;

// ✅ 方法2:extendsで制約を付ける
const identity = <T extends unknown>(arg: T): T => arg;

筆者のおすすめは方法1の<T,>です。 記述量が最小で、チーム内でも意図が伝わりやすいからです。 上記のDataFetcherやGenericTableでもこの書き方を採用しています。 .tsxファイルでアロー関数にジェネリクスを使う場合は、 常にこのパターンを適用してください。

高度なジェネリクスパターン

ジェネリクスの基本を押さえたら、 次はConditional TypesやMapped Typesとの組み合わせに進みましょう。 これらを使いこなすと、型レベルでのロジック表現が可能になります。

Conditional TypesとMapped Typesの組み合わせ

Conditional Types(条件付き型)は、三項演算子に似た構文で型を分岐させる仕組みです(TypeScript公式ドキュメント)。

// 条件付き型の基本構文
type IsString<T> = T extends string ? "文字列です" : "文字列ではありません";

type A = IsString<string>;  // "文字列です"
type B = IsString<number>;  // "文字列ではありません"

Union型に対しては分配的に適用される点がポイントです。 IsString<string | number>"文字列です" | "文字列ではありません"に展開されます。

Mapped Types(マップ型)と組み合わせると、オブジェクトの各プロパティを条件に応じて変換できます(TypeScript公式ドキュメント)。

// string型のプロパティだけをオプショナルにする
type OptionalStrings<T> = {
  [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
}

// name, emailだけundefined許容になる
type FlexibleUser = OptionalStrings<User>;

inferキーワードによる型抽出

inferは、Conditional Types内で型を「キャプチャ」するキーワードです。 関数の戻り値やPromiseの中身を取り出す場面で活躍します。

// 関数の戻り値型を抽出する(Returntype<T>の自作版)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Result = MyReturnType<() => { success: boolean }>;
// { success: boolean }

// Promiseの中身を再帰的に取り出す
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;

type Data = UnwrapPromise<Promise<Promise<string>>>;
// string

組み込みのReturnType<T>Parameters<T>も、 内部ではinferで実装されています。 仕組みを知っておくと、独自のユーティリティ型を設計する際に役立ちます。

共変性・反変性の理解とin/outキーワード

型パラメータの「向き」を理解しておくと、型エラーの原因が見えやすくなります。 Dmitri Pavlutin氏の解説によると、 関数型はパラメータ型について反変(contravariant)、 戻り値型について共変(covariant)です。

かんたんに言うと、以下のルールです。

  • 共変(covariant): 出力側。Dog extends AnimalならProducer<Dog>Producer<Animal>に代入できます
  • 反変(contravariant): 入力側。Dog extends AnimalならConsumer<Animal>Consumer<Dog>に代入できます

TypeScript 5.0以降では、outinキーワードでこの関係を明示できます。

// outで共変を宣言(読み取り専用)
interface Producer<out T> {
  get(): T;
}

// inで反変を宣言(書き込み専用)
interface Consumer<in T> {
  accept(value: T): void;
}

in/outを付けると、宣言と矛盾する使い方をした場合にコンパイラが警告してくれます。 ライブラリ設計やAPIの型定義で、 意図しない型の代入を防ぐ安全装置として機能します。

トラブルシューティング:よくあるエラーと対処法

ジェネリクスを使い始めると、特有のエラーに遭遇します。 ここでは実務で頻出する3つのパターンと解決策を紹介します。

「Type is not assignable」エラーの原因と解決策

ジェネリクスで最も多いエラーがこれです。 T extends Aと制約をつけても、 TAより多くのプロパティを持つ可能性があります。 そのため、Aに適合するオブジェクトをそのままTに代入できません (WebDevTutor)。

// ❌ エラーになるパターン
function createUser<T extends { name: string }>(name: string): T {
  return { name }; // Error: { name: string } is not assignable to T
}

// ✅ コールバック関数で型変換する解決策
function createUser<T extends { name: string }>(
  name: string,
  factory: (base: { name: string }) => T
): T {
  return factory({ name });
}

ポイントは「TAのサブタイプかもしれない」という視点です。コールバックやas Tで明示的に変換しましょう。

制約なし型パラメータのプロパティアクセスエラー

型パラメータに制約がないと、コンパイラはunknownと同様に扱います。 プロパティへのアクセスはすべてエラーになります (サバイバルTypeScript)。

// ❌ 制約なし:プロパティにアクセスできない
function getLength<T>(value: T): number {
  return value.length; // Error: Property 'length' does not exist on type 'T'
}

// ✅ extendsで制約を追加
function getLength<T extends { length: number }>(value: T): number {
  return value.length; // OK
}

extendsによる制約の追加が基本的な解決策です。 配列や文字列を受け取りたい場合は { length: number }のような構造的な型を指定してください。

GitHub Issues・Stack Overflowで頻出する問題パターン

コミュニティで繰り返し議論されている問題を2つ押さえておきましょう。

1. Union型メンバーへのナローイングが効かない問題

GitHub Issue #27808で 議論されているextends oneof構文の提案です。 現状、ジェネリック制約にUnion型を指定しても、個別のメンバーへ絞り込めません。 回避策はオーバーロードや関数の分割です。

2. extendsで余分なプロパティが検出されない問題

GitHub Issue #35899では、 ジェネリック制約が「exact types」(厳密な型一致)を サポートしない制限が指摘されています。 T extends Aは「Aのプロパティを最低限持つ」意味であり、 余分なプロパティは許容されます。 意図しないプロパティの混入を防ぐには、 Mapped Typesとkeyofを組み合わせた型レベルのバリデーションが有効です。

いずれも2026年2月時点で未解決のため、今後のTypeScriptアップデートを注視してください。

まとめ:ジェネリクス習得の次のステップ

ジェネリクスを実務で使いこなすには「段階的に適用範囲を広げる」アプローチが効果的です。

まず取り組みたいのは、 既存コードのany型をジェネリクスに置き換えるリファクタリングです。 APIレスポンスの型定義やデータ変換関数など、 型情報が失われている箇所を洗い出してみてください。 ApiResponse<T>のようなパターンを1つ導入するだけで、 補完の精度が体感できるほど変わります。

次の段階として、ユーティリティ型の自作に挑戦してみましょう。 PartialPickのソースコードを読むと、 Mapped TypesとConditional Typesの実践的な組み合わせ方が見えてきます。 TypeScript公式の 型チャレンジリポジトリは、 こうした高度なパターンを段階的に学べる良い教材です。

TypeScript 7.0ではGo実装による最大10倍の高速化が見込まれています (Zenn)。 メモリ使用量も約50%削減される見通しです。 型推論の性能向上も期待されており、 ジェネリクスをより積極的に活用できる環境が整いつつあります。

筆者の見解では、ジェネリクスの設計で最も大切なのは「使う側の体験」です。 型パラメータが3つ以上になったら、 デフォルト型パラメータで省略可能にする、あるいは設計そのものを見直す。 こうした判断基準を持つことが、型安全性と可読性のバランスにつながります。

参考文献

s

この記事を書いた人

数学科出身のWebエンジニア

共有: