•13 min read

TypeScriptジェネリクス: 型安全なAPIのための高度なパターン

TypeScriptジェネリクス: 型安全なAPIのための高度なパターン

TypeScriptのジェネリクスは、柔軟で再利用可能、かつ型安全なコードを書く上での要です。基本的なジェネリクスは比較的簡単ですが、高度なジェネリックパターンを習得することで、TypeScriptの真の力を引き出すことができます。2026年には、アプリケーションがより複雑になるにつれて、これらの高度なテクニックを活用することはもはや選択肢ではなく、堅牢なコードベースを維持するために不可欠となります。

この詳細な解説では、高度なジェネリックパターンを探求し、適応性の高いAPI、ユーティリティ型、ジェネリックコンポーネントを作成することで、ランタイムエラーを劇的に削減し、開発者エクスペリエンスを向上させる方法を学びます。

Audio Briefing
0:00 / 0:00

基本: 「型変数」を理解する

高度なパターンに飛び込む前に、核となる概念を簡単に復習しましょう。ジェネリクスを使用すると、ユーザーが提供する型をキャプチャし、それを使用して引数、戻り値、またはプロパティの型を決定できます。

function identity<T>(arg: T): T {
    return arg;
}

const num = identity<number>(42); // T is number
const str = identity("Hello");    // T is inferred as "Hello" (literal type)
Advertisement

1. ジェネリック制約 (extends)

多くの場合、ジェネリックが任意の型であることを望まず、特定のプロパティを持つことを必要とします。ここで、extendsキーワードを使用したジェネリック制約が登場します。

オブジェクトの形状への制約

interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(arg: T): T {
    console.log(arg.length);
    return arg;
}

logLength("string"); // OK, strings have .length
logLength([1, 2, 3]); // OK, arrays have .length
// logLength(10);    // Error: number doesn't have .length

キーによる制約 (keyof)

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}

const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // OK, returns string
getProperty(user, "age");  // OK, returns number
// getProperty(user, "email"); // Error: 'email' doesn't exist

これにより、オブジェクトプロパティに動的にアクセスする際の絶対的な型安全性が保証されます。

2. 条件型 (T extends U ? X : Y)

条件型を使用すると、条件に基づいて非均一な型マッピングを表現できます。これらは多くの高度なユーティリティ型の基礎を形成します。

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

条件型内での推論 (infer)

inferキーワードは、別の型の中から型を抽出します。これは、Promiseのアンラップ、関数の戻り値の型の抽出、配列の分解に非常に強力です。

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type FetchedData = UnwrapPromise<Promise<{ id: number; name: string }>>;
// FetchedData is { id: number; name: string }

// Extract a function's first argument type:
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Fn = (id: number, name: string) => void;
type IdType = FirstArg<Fn>; // number

分配条件型

条件型をユニオンに適用すると、TypeScriptはそれを各メンバーに分配します。

type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // string[] | number[]
// NOT (string | number)[]

// Prevent distribution by wrapping in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Both = ToArrayNonDist<string | number>; // (string | number)[]

ユニオンを操作するユーティリティ型を作成する際には、分配を理解することが重要です。

3. マップ型

マップ型は、既存の型に基づいて、そのキーを反復処理することで新しい型を作成します。

type MyReadonly<T> = {
    readonly [P in keyof T]: T[P];
};

type Mutable<T> = {
    -readonly [P in keyof T]: T[P]; // remove readonly
};

type RequiredProperties<T> = {
    [P in keyof T]-?: T[P]; // remove optional
};

// Remap keys using 'as'
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
Advertisement

4. テンプレートリテラル型

テンプレートリテラル型を使用すると、テンプレート構文を使用して文字列リテラル型を構成できます。これはTypeScriptの最も強力な最近の追加機能の1つです。

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
type ChangeEvent = EventName<'change'>; // "onChange"

// Build a full event-handler map from a union:
type AllHandlers = {
    [K in 'click' | 'focus' | 'blur' as EventName<K>]?: (e: Event) => void;
};
// { onClick?: ...; onFocus?: ...; onBlur?: ...; }

このパターンは、型安全なイベントシステム、CSS-in-JSクラスビルダー、APIパスコンストラクターをランタイムオーバーヘッドなしで実現します。

5. ドメイン安全性のためのブランデッド型

TypeScriptの構造的型システムは、同じ形状を持つ2つの型が交換可能であることを意味します。たとえ一方がUserIdで、もう一方がOrderIdであってもです。ブランデッド型(不透明型とも呼ばれます)は、意図しない混同を防ぎます。

declare const __brand: unique symbol;
type Brand<T, B> = T & { [__brand]: B };

type UserId  = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const uid  = "u_123" as UserId;
const oid  = "o_456" as OrderId;

getUser(uid);   // ✅ OK
getOrder(oid);  // ✅ OK
// getUser(oid); // ❌ Error: OrderId is not assignable to UserId

ID、通貨量、検証済み文字列(例: EmailAddress)、または構造的に同一の2つの型を混同してはならないドメイン概念には、ブランドを使用してください。

6. 判別共用体 + 網羅性チェック

判別共用体は、kind(またはtype)リテラルとペイロードを組み合わせることで、TypeScriptがswitchステートメントで自動的に型を絞り込むことを可能にします。

type Circle    = { kind: 'circle';    radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Triangle  = { kind: 'triangle';  base: number; height: number };

type Shape = Circle | Rectangle | Triangle;

function area(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':    return Math.PI * shape.radius ** 2;
        case 'rectangle': return shape.width * shape.height;
        case 'triangle':  return 0.5 * shape.base * shape.height;
        default:
            // exhaustiveness check — compile error if a new Shape is added
            const _exhaustive: never = shape;
            throw new Error(`Unhandled shape: ${JSON.stringify(_exhaustive)}`);
    }
}

デフォルトブランチのnever代入が鍵です。ユニオンにtype Triangle2 = { kind: 'triangle2'; ... }を追加し、caseを追加し忘れた場合、コードが実行される前にTypeScriptが_exhaustive行でエラーを発生させます。

7. 可変長タプル型

TypeScript 4以降では、ジェネリクスをタプルにスプレッドする機能がサポートされており、引数リストを転送または変換する関数の正確な型推論を可能にします。

type Prepend<T, Tuple extends unknown[]> = [T, ...Tuple];

function withLogger<Args extends unknown[], R>(
    fn: (...args: Args) => R,
    label: string
): (...args: Args) => R {
    return (...args: Args) => {
        console.log(`[${label}]`, ...args);
        return fn(...args);
    };
}

const add = (a: number, b: number) => a + b;
const loggedAdd = withLogger(add, 'add');
loggedAdd(2, 3); // TypeScript knows this takes (number, number), not (any, any)

8. ジェネリクスを使用したビルダーパターン

ジェネリクスは、メソッドが連鎖するにつれて型コンテキストが進化する流暢なAPIを構築する際に威力を発揮します。

class QueryBuilder<T = Record<string, unknown>> {
    private query: Record<string, unknown> = {};

    select<K extends keyof T>(keys: K[]): QueryBuilder<Pick<T, K>> {
        this.query.select = keys;
        return this as unknown as QueryBuilder<Pick<T, K>>;
    }

    where(condition: Partial<T>): QueryBuilder<T> {
        this.query.where = condition;
        return this;
    }

    execute(): Promise<T[]> {
        // Implementation
        return Promise.resolve([]);
    }
}

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

// Result is inferred as Promise<Pick<User, 'id' | 'name'>[]>
const result = new QueryBuilder<User>()
    .select(['id', 'name'])
    .where({ age: 30 })
    .execute();

9. 実世界: 型安全なAPIレスポンスラッパー

条件型、ブランデッド型、判別共用体を組み合わせて、本番APIクライアントのための完全なパターンを以下に示します。

// Branded request IDs prevent mixing correlation identifiers
type RequestId = Brand<string, 'RequestId'>;

// Discriminated union response envelope
type ApiResponse<T> =
    | { ok: true;  data: T;       requestId: RequestId }
    | { ok: false; error: string; requestId: RequestId };

// Generic fetch wrapper with full type inference
async function apiFetch<T>(
    url: string,
    schema: (raw: unknown) => T   // runtime validator (e.g. zod.parse)
): Promise<ApiResponse<T>> {
    const requestId = crypto.randomUUID() as RequestId;
    try {
        const res = await fetch(url);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const raw = await res.json();
        return { ok: true, data: schema(raw), requestId };
    } catch (err) {
        return { ok: false, error: String(err), requestId };
    }
}

// Usage — TypeScript infers the shape of `user` from the schema function
const resp = await apiFetch('/api/users/1', (raw) => {
    if (typeof raw !== 'object' || !raw) throw new Error('invalid');
    return raw as { id: number; name: string };
});

if (resp.ok) {
    console.log(resp.data.name); // fully typed
} else {
    console.error(resp.error);
}

anyも、呼び出しサイトでのキャストもありません。そして、すべての成功パスがdataを処理し、すべての失敗パスがerrorを処理するというコンパイル時の証明が得られます。

10. Reactにおける高階コンポーネント (HOC)

Reactで作業する場合、ジェネリクスは高階コンポーネントで型安全性を維持します。

import React from 'react';

interface WithLoadingProps {
    isLoading: boolean;
}

function withLoading<P extends object>(
    WrappedComponent: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
    return function WithLoadingComponent({ isLoading, ...props }: P & WithLoadingProps) {
        if (isLoading) return <div>Loading...</div>;
        return <WrappedComponent {...(props as P)} />;
    };
}

interface ProfileProps { name: string; }
const Profile = ({ name }: ProfileProps) => <div>{name}</div>;
const ProfileWithLoading = withLoading(Profile);

// <ProfileWithLoading isLoading={true} name="Alice" />
// TypeScript enforces both isLoading and name — nothing is lost.

クイックリファレンス

パターンユースケース
extends制約Tを既知のプロパティを持つ形状に制限する
条件型におけるinfer内部型(Promiseの結果、戻り値の型)を抽出する
分配条件型ユニオンメンバー全体に条件を適用する
マップ型 + as型レベルでキーの名前を変更または変換する
テンプレートリテラル型型安全な文字列合成(イベント、ルート)
ブランデッド型構造的に同一のドメイン型の混同を防ぐ
判別共用体 + never網羅性チェックされたswitchステートメント
可変長タプル高階関数で引数の数を維持する
ビルダーパターンメソッド呼び出しごとに型が進化する流暢なAPI

結論

TypeScriptのジェネリクスを習得することで、型チェックされたJavaScriptを書くことから、非常に堅牢で自己文書化された回復力のあるAPIを設計することへと移行できます。ここで取り上げたパターン — 条件型、infer、テンプレートリテラル、ブランデッド型、判別共用体 — は、コンパイル時にゼロランタイムオーバーヘッドで、あらゆる種類のバグを捕捉することを可能にします。

より複雑なライブラリやアプリケーションを構築するにつれて、これらの高度なジェネリックパターンは、TypeScriptの武器庫に不可欠なツールとなるでしょう。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement