【TypeScript】高度な型(条件型・テンプレートリテラル型・マップ型)完全ガイド|実務で使える応用テクニック

【TypeScript】高度な型(条件型・テンプレートリテラル型・マップ型)完全ガイド|実務で使える応用テクニック TypeScript

TypeScriptの高度な型(Advanced Types)は、型システムの真の力を引き出すための機能群です。条件型(Conditional Types)、マップ型(Mapped Types)、テンプレートリテラル型(Template Literal Types)を組み合わせることで、JavaScriptでは実現できなかった型レベルのプログラミングが可能になります。

この記事では、TypeScriptの高度な型機能を基礎から実務レベルまで完全解説します。各トピックには豊富なコード例を掲載し、実際のプロジェクトですぐに使えるテクニックを網羅しています。

この記事で学べること

  • 条件型(Conditional Types)の基本から infer・分配条件型まで
  • マップ型(Mapped Types)によるオブジェクト型の柔軟な変換
  • テンプレートリテラル型で文字列パターンの型安全な定義
  • インデックスアクセス型でネストした型の取得
  • 高度な型を組み合わせた実務パターン(DeepPartial、イベントシステム、APIレスポンス変換)
  • 型レベルプログラミングのパフォーマンスとデバッグ
型の種類 構文 主な用途
条件型T extends U ? X : Y型に応じた分岐処理
マップ型{[K in keyof T]: ...}オブジェクト型の変換
テンプレートリテラル型`${A}-${B}`文字列パターンの型定義
インデックスアクセス型T[K] / T[number]型の部分的な取得
inferT extends (infer U)[] ? U : T条件型内での型の推論・抽出
スポンサーリンク
  1. 高度な型とは? ── 基本型から応用型への進化
  2. 条件型(Conditional Types)
    1. 基本構文と使い方
    2. infer キーワードで型を抽出する
    3. 分配条件型(Distributive Conditional Types)
    4. ネストした条件型
    5. 条件型と infer を使った実用ユーティリティ型
  3. マップ型(Mapped Types)
    1. 基本構文 { [K in keyof T]: … }
    2. 修飾子(readonly, optional)の追加・削除
    3. as句でキーの変換(Key Remapping)
    4. テンプレートリテラルとの組み合わせ
  4. テンプレートリテラル型(Template Literal Types)
    1. 基本構文と文字列パターン
    2. ユニオン型との組み合わせで型を生成
    3. Uppercase / Lowercase / Capitalize / Uncapitalize
    4. イベント名やCSSプロパティの型安全な定義
  5. インデックスアクセス型(Indexed Access Types)
    1. T[K] での型の取得
    2. 配列要素の型取得 T[number]
    3. ネストしたプロパティの型取得
  6. 条件型 x マップ型 x テンプレートリテラル型の組み合わせ
    1. DeepPartial, DeepReadonly の実装
    2. 型安全なイベントシステムの構築
    3. APIレスポンスの型変換
  7. 実務パターン集
    1. フォームバリデーションの型
    2. 型安全なルーティング
    3. Builder パターンの型付け
    4. 型レベルのバリデーション
  8. パフォーマンスと制限
    1. 再帰型の深度制限
    2. 型の複雑さとコンパイル速度
    3. デバッグテクニック
  9. TypeScript 組み込みユーティリティ型の完全リファレンス
  10. 高度な型の実践例: 型安全な状態管理
  11. TypeScript 高度な型チートシート
  12. まとめ

高度な型とは? ── 基本型から応用型への進化

TypeScriptの型システムは、単なるstringnumberのようなプリミティブ型から始まり、ユニオン型ジェネリクスを経て、さらに高度な型操作へと進化します。この進化の先にあるのが、今回解説する高度な型です。

高度な型を使うことで、以下のようなことが型レベルで実現できます。

TypeScript – 高度な型でできること
// 1. 条件に応じて型を切り替える(条件型)
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;  // true
type B = IsString<42>;       // false

// 2. オブジェクトの全プロパティを変換する(マップ型)
type Optional<T> = { [K in keyof T]?: T[K] };
// { name?: string; age?: number; }

// 3. 文字列パターンを型で表現する(テンプレートリテラル型)
type EventName = `on${"Click" | "Hover" | "Focus"}`;
// "onClick" | "onHover" | "onFocus"

// 4. 関数の戻り値型を自動抽出する(infer)
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type C = ReturnOf<() => string>;  // string

これらの型機能を組み合わせることで、型安全なAPIクライアント、フォームバリデーション、ルーティングシステムなど、実務で強力に活用できる型定義を構築できます。

前提知識:この記事ではジェネリクス<T>)とユニオン型A | B)の基本を理解していることを前提としています。これらに不安がある方は、先にジェネリクスの基礎を学んでから読み進めることをおすすめします。

条件型(Conditional Types)

条件型(Conditional Types)は、TypeScript 2.8で導入された機能で、型レベルの三項演算子として機能します。型の関係に基づいて、異なる型を返すことができます。

基本構文と使い方

条件型の基本構文は T extends U ? X : Y です。これは「型 T が型 U に割り当て可能なら型 X を、そうでなければ型 Y を返す」という意味です。

TypeScript – 条件型の基本構文
// 基本構文
type ConditionalType = SomeType extends OtherType ? TrueType : FalseType;

// 具体例: 型が string かどうかを判定
type IsString<T> = T extends string ? "string型です" : "string型ではありません";

type Test1 = IsString<"hello">;   // "string型です"
type Test2 = IsString<123>;       // "string型ではありません"
type Test3 = IsString<string>;    // "string型です"
type Test4 = IsString<boolean>;   // "string型ではありません"

条件型はジェネリクスと組み合わせることで真価を発揮します。入力された型に応じて動的に戻り値の型を変更できます。

TypeScript – 条件型とジェネリクスの組み合わせ
// 型に応じてラッパーの形を変える
type Wrap<T> = T extends string
  ? { value: string; length: number }
  : T extends number
  ? { value: number; isPositive: boolean }
  : { value: T };

type StringWrap = Wrap<string>;   // { value: string; length: number }
type NumberWrap = Wrap<number>;   // { value: number; isPositive: boolean }
type BoolWrap = Wrap<boolean>;   // { value: boolean }

// 実務例: API レスポンスの型を切り替える
type ApiResponse<T> = T extends "user"
  ? { id: number; name: string; email: string }
  : T extends "product"
  ? { id: number; title: string; price: number }
  : never;

function fetchData<T extends "user" | "product">(endpoint: T): Promise<ApiResponse<T>> {
  return fetch(`/api/${endpoint}`).then(r => r.json());
}

// 型推論が正確に行われる
const user = await fetchData("user");       // { id: number; name: string; email: string }
const product = await fetchData("product");  // { id: number; title: string; price: number }

上の例では、fetchData 関数の引数に "user" を渡すと戻り値がユーザー型に、"product" を渡すと商品型に自動的に推論されます。これが条件型の強力な型の絞り込み機能です。

注意:条件型の extends は、クラスの継承における extends とは異なります。ここでは「割り当て可能(assignable)」かどうかを判定するキーワードとして機能します。T extends U は「T が U のサブタイプである」という意味です。

infer キーワードで型を抽出する

infer キーワードは条件型の中で使用し、型パターンの一部を変数として抽出する機能です。これにより、関数の戻り値型、配列の要素型、Promiseの解決値型などを取り出すことができます。

TypeScript – infer の基本
// 関数の戻り値型を抽出
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function greet(name: string): string {
  return `Hello, ${name}!`;
}

type GreetReturn = MyReturnType<typeof greet>;  // string

// 配列の要素型を抽出
type ElementType<T> = T extends (infer U)[] ? U : never;

type StrArray = ElementType<string[]>;           // string
type NumArray = ElementType<number[]>;           // number
type MixedArray = ElementType<(string | number)[]>;  // string | number
type NotArray = ElementType<string>;             // never(配列でないため)

// Promise の解決値型を抽出
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>;  // string
type P2 = UnwrapPromise<Promise<number>>;  // number
type P3 = UnwrapPromise<string>;           // string(Promiseでない場合はそのまま)

infer は条件型の extends 句の中でのみ使用できます。infer R は「ここに来る型を R として推論する」という宣言です。マッチしなかった場合は false 分岐(通常は never)が返されます。

TypeScript – infer の応用パターン
// 関数の第1引数の型を取得
type FirstArg<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never;

type Arg = FirstArg<(name: string, age: number) => void>; // string

// コンストラクタの引数型を取得
type ConstructorArgs<T> = T extends new (...args: infer A) => any ? A : never;

class User {
  constructor(public name: string, public age: number) {}
}

type UserArgs = ConstructorArgs<typeof User>; // [name: string, age: number]

// タプルの先頭と残りを分離
type Head<T extends any[]> = T extends [infer First, ...any[]] ? First : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;

type H = Head<[string, number, boolean]>; // string
type T = Tail<[string, number, boolean]>; // [number, boolean]

// 複数の infer を使う: オブジェクトのキーと値の型を同時に抽出
type ExtractEntry<T> = T extends Map<infer K, infer V>
  ? { key: K; value: V }
  : never;

type Entry = ExtractEntry<Map<string, number>>; // { key: string; value: number }

ポイント:infer は TypeScript の組み込みユーティリティ型(ReturnTypeParametersConstructorParameters など)の実装にも使われています。自分で同等の型を作れるようになると、型プログラミングの幅が大きく広がります。

分配条件型(Distributive Conditional Types)

分配条件型は、条件型にユニオン型を渡したとき、ユニオンの各メンバーに対して個別に条件が評価される振る舞いです。これはTypeScriptの条件型の重要な特性で、多くのユーティリティ型の基盤となっています。

TypeScript – 分配条件型の仕組み
// 分配の仕組みを理解する
type ToArray<T> = T extends any ? T[] : never;

// ユニオン型を渡すと、各メンバーに対して個別に適用される
type Result = ToArray<string | number>;
// 分配: ToArray<string> | ToArray<number>
// 結果: string[] | number[]

// (string | number)[] ではなく string[] | number[] になることに注意

// 分配条件型の活用: ユニオンから特定の型を除外する
type MyExclude<T, U> = T extends U ? never : T;

type Colors = "red" | "green" | "blue" | "yellow";
type WarmColors = MyExclude<Colors, "green" | "blue">;
// 結果: "red" | "yellow"

// ユニオンから特定の型を抽出する
type MyExtract<T, U> = T extends U ? T : never;

type CoolColors = MyExtract<Colors, "green" | "blue" | "purple">;
// "green" | "blue"

分配条件型は、裸の型パラメータ(naked type parameter)に対してのみ発生します。型パラメータをタプルやオブジェクトで包むと分配を防ぐことができます。

TypeScript – 分配を防ぐテクニック
// 分配される(裸の型パラメータ)
type Distributed<T> = T extends any ? T[] : never;
type D = Distributed<string | number>; // string[] | number[]

// 分配されない(タプルで包む)
type NonDistributed<T> = [T] extends [any] ? T[] : never;
type ND = NonDistributed<string | number>; // (string | number)[]

// 分配を利用: ユニオンから null/undefined を除去
type MyNonNullable<T> = T extends null | undefined ? never : T;
type Cleaned = MyNonNullable<string | null | undefined | number>;
// string | number

分配条件型の動作まとめ

  • T extends U ? X : Yユニオン型を渡すと、各メンバーに対して個別に評価される
  • 分配が起きる条件:型パラメータが(直接 extends の左辺にある)であること
  • [T] extends [U] のようにタプルで包むと分配を防止できる
  • ExcludeExtractNonNullable などの組み込み型は分配条件型で実装されている

ネストした条件型

条件型はネスト(入れ子)にすることで、複数段階の型の分岐を表現できます。JavaScriptの if-else if-else チェーンと同様の役割を型レベルで果たします。

TypeScript – ネストした条件型
// TypeScriptの型名を文字列リテラルとして返す
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T1 = TypeName<string>;       // "string"
type T2 = TypeName<42>;           // "number"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<string[]>;     // "object"

// 実務例: DBカラム型をTSの型に変換
type DbColumnType = "VARCHAR" | "INT" | "BOOLEAN" | "TIMESTAMP" | "JSON";

type DbToTs<T extends DbColumnType> =
  T extends "VARCHAR" ? string :
  T extends "INT" ? number :
  T extends "BOOLEAN" ? boolean :
  T extends "TIMESTAMP" ? Date :
  T extends "JSON" ? Record<string, unknown> :
  never;

type VarcharTs = DbToTs<"VARCHAR">;    // string
type IntTs = DbToTs<"INT">;          // number
type TimestampTs = DbToTs<"TIMESTAMP">;  // Date

// ネスト + infer: 深い型の再帰展開
type DeepUnwrap<T> =
  T extends Promise<infer U> ? DeepUnwrap<U> :
  T extends (infer U)[] ? DeepUnwrap<U> :
  T;

type Deep1 = DeepUnwrap<Promise<string>>;         // string
type Deep2 = DeepUnwrap<Promise<Promise<number>>>; // number
type Deep3 = DeepUnwrap<string[][]>;            // string

注意:再帰的な条件型は TypeScript 4.1 以降で対応していますが、再帰の深さには上限があります(デフォルトで約1000レベル)。無限再帰を避けるために、必ずベースケース(終了条件)を設けてください。

条件型と infer を使った実用ユーティリティ型

ここでは、条件型と infer を組み合わせた実用的なユーティリティ型をまとめて紹介します。これらのパターンは実際のプロジェクトで繰り返し活用できるものです。

TypeScript – 実用ユーティリティ型集
// 1. 関数のパラメータ型を取得(Parameters と同等)
type MyParameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number, email: string) {
  return { name, age, email };
}
type CreateUserParams = MyParameters<typeof createUser>;
// [name: string, age: number, email: string]

// 2. Promise を再帰的にアンラップ(Awaited と同等)
type MyAwaited<T> =
  T extends null | undefined ? T :
  T extends object & { then(onfulfilled: infer F): any }
    ? F extends (value: infer V) => any ? MyAwaited<V> : never
    : T;

type A1 = MyAwaited<Promise<string>>;           // string
type A2 = MyAwaited<Promise<Promise<number>>>; // number

// 3. ユニオンをインターセクションに変換
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends
  (k: infer I) => void ? I : never;

type Intersected = UnionToIntersection<
  { a: string } | { b: number } | { c: boolean }
>;
// { a: string } & { b: number } & { c: boolean }

// 4. Flatten: 配列型を展開する
type Flatten<T> = T extends (infer U)[] ? U : T;

type F1 = Flatten<string[]>;   // string
type F2 = Flatten<number[][]>; // number[]
type F3 = Flatten<string>;     // string(配列でなければそのまま)
ユーティリティ型機能使用パターン
ReturnType<T>関数の戻り値型を取得ReturnType<typeof fn>
Parameters<T>関数の引数型をタプルで取得Parameters<typeof fn>
Awaited<T>Promiseの解決値型を再帰的に取得Awaited<Promise<T>>
Exclude<T, U>ユニオンから特定の型を除外Exclude<A | B | C, B>
Extract<T, U>ユニオンから特定の型を抽出Extract<A | B | C, B>
NonNullable<T>nullとundefinedを除外NonNullable<T | null>

マップ型(Mapped Types)

マップ型(Mapped Types)は、既存の型のプロパティを反復処理して新しい型を生成する機能です。オブジェクト型のすべてのプロパティに対して統一的な変換を適用できます。TypeScript 2.1で導入されました。

基本構文 { [K in keyof T]: … }

マップ型の基本構文は { [K in keyof T]: NewType } です。keyof T で型 T のすべてのキーを取得し、in で各キーを反復処理します。

TypeScript – マップ型の基本
// 基本構文
type MappedType<T> = {
  [K in keyof T]: SomeTransformation<T[K]>
};

// 例: すべてのプロパティを string に変換
type Stringify<T> = {
  [K in keyof T]: string
};

interface User {
  id: number;
  name: string;
  isActive: boolean;
}

type StringifiedUser = Stringify<User>;
// { id: string; name: string; isActive: string }

// 例: すべてのプロパティを配列に変換
type Arrayify<T> = {
  [K in keyof T]: T[K][]
};

type ArrayUser = Arrayify<User>;
// { id: number[]; name: string[]; isActive: boolean[] }

// 例: Nullable に変換(各プロパティに null を追加)
type Nullable<T> = {
  [K in keyof T]: T[K] | null
};

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; isActive: boolean | null }

ポイント:T[K] はインデックスアクセス型(後述)で、キー K に対応する値の型を取得します。マップ型の中で T[K] を使うことで、元の型の値を参照しながら変換できます。

修飾子(readonly, optional)の追加・削除

マップ型では、readonly?(optional)の修飾子を追加・削除できます。+ を前置すると追加、- を前置すると削除です。

TypeScript – 修飾子の追加と削除
// すべてのプロパティを readonly にする(Readonly と同等)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
};

// すべてのプロパティをオプショナルにする(Partial と同等)
type MyPartial<T> = {
  [K in keyof T]?: T[K]
};

// すべてのプロパティを必須にする(Required と同等)
type MyRequired<T> = {
  [K in keyof T]-?: T[K]
};

// readonly を削除する(Mutable)
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
};

// 使用例
interface Config {
  readonly host: string;
  readonly port: number;
  readonly debug?: boolean;
}

type MutableConfig = Mutable<Config>;
// { host: string; port: number; debug?: boolean }

type RequiredMutableConfig = MyRequired<Mutable<Config>>;
// { host: string; port: number; debug: boolean }

// 実務例: フォームの入力状態
interface UserForm {
  username: string;
  email: string;
  password: string;
}

// フォームの各フィールドの状態を表す型
type FormState<T> = {
  [K in keyof T]: {
    value: T[K];
    error: string | null;
    touched: boolean;
  }
};

type UserFormState = FormState<UserForm>;
// {
//   username: { value: string; error: string | null; touched: boolean };
//   email: { value: string; error: string | null; touched: boolean };
//   password: { value: string; error: string | null; touched: boolean };
// }
修飾子構文効果対応する組み込み型
readonlyreadonly [K in keyof T]読み取り専用を追加Readonly<T>
-readonly-readonly [K in keyof T]読み取り専用を削除(なし)
?[K in keyof T]?オプショナルを追加Partial<T>
-?[K in keyof T]-?オプショナルを削除Required<T>

as句でキーの変換(Key Remapping)

TypeScript 4.1で導入された as 句を使うと、マップ型の中でキーの名前を変換できます。テンプレートリテラル型と組み合わせることで、getter/setterパターンやイベントハンドラの型を自動生成できます。

TypeScript – Key Remapping(as句)
// 基本構文: as 句でキーを変換
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
  name: string;
  age: number;
  location: string;
}

type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

// Setters も同様に生成
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};

type PersonSetters = Setters<Person>;
// {
//   setName: (value: string) => void;
//   setAge: (value: number) => void;
//   setLocation: (value: string) => void;
// }

// as 句で never を返すとキーを除外できる
type RemoveKind<T> = {
  [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Shape {
  kind: "circle";
  radius: number;
  color: string;
}

type ShapeWithoutKind = RemoveKind<Shape>;
// { radius: number; color: string }

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

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  email: string;
  count: number;
}

type StringProps = PickByType<Mixed, string>;
// { name: string; email: string }

type NumberProps = PickByType<Mixed, number>;
// { id: number; count: number }

Key Remapping のポイント

  • as 句の中で never を返すと、そのキーは結果の型から除外される
  • Capitalize などの組み込み文字列型と組み合わせてキー名を変換できる
  • 条件型と組み合わせることで、値の型に基づいてキーをフィルタリングできる
  • string & K は、Ksymbol の場合を除外するためのイディオム

テンプレートリテラルとの組み合わせ

マップ型とテンプレートリテラル型を組み合わせると、既存のオブジェクト型からイベントハンドラ型CSS-in-JS型を自動生成できます。

TypeScript – マップ型 + テンプレートリテラル
// イベントハンドラ型の自動生成
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (newValue: T[K]) => void
};

interface State {
  name: string;
  count: number;
  active: boolean;
}

type StateHandlers = EventHandlers<State>;
// {
//   onNameChange: (newValue: string) => void;
//   onCountChange: (newValue: number) => void;
//   onActiveChange: (newValue: boolean) => void;
// }

// プレフィックス付きのプロパティを生成
type Prefixed<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}_${string & K}`]: T[K]
};

interface DbRow {
  id: number;
  name: string;
}

type UserRow = Prefixed<DbRow, "user">;
// { user_id: number; user_name: string }

// 実務例: React の状態管理パターン
type StateWithSetters<T> = T & {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};

type AppStore = StateWithSetters<{
  theme: "light" | "dark";
  locale: string;
  isLoggedIn: boolean;
}>;
// {
//   theme: "light" | "dark";
//   locale: string;
//   isLoggedIn: boolean;
//   setTheme: (value: "light" | "dark") => void;
//   setLocale: (value: string) => void;
//   setIsLoggedIn: (value: boolean) => void;
// }

テンプレートリテラル型(Template Literal Types)

テンプレートリテラル型は TypeScript 4.1 で導入された機能で、JavaScriptのテンプレートリテラル構文(バッククォート)を型レベルで使用できます。文字列パターンを型として表現し、型安全な文字列操作が実現できます。

基本構文と文字列パターン

テンプレートリテラル型は、バッククォートの中に ${} で型を埋め込むことで、文字列のパターンを型として定義します。

TypeScript – テンプレートリテラル型の基本
// 基本: 文字列リテラル型の結合
type Greeting = `Hello, ${string}`;

const a: Greeting = "Hello, World";     // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, World";        // エラー!

// リテラル型の結合
type World = "world";
type HelloWorld = `hello ${World}`;  // "hello world"

// 数値型との組み合わせ
type Id = `user-${number}`;
const id1: Id = "user-123";  // OK
const id2: Id = "user-456";  // OK
// const id3: Id = "user-abc";  // エラー!

// CSSの単位付き値
type CSSUnit = "px" | "em" | "rem" | "%" | "vh" | "vw";
type CSSLength = `${number}${CSSUnit}`;

const width: CSSLength = "100px";   // OK
const height: CSSLength = "50vh";   // OK
const margin: CSSLength = "2.5rem"; // OK
// const bad: CSSLength = "auto";     // エラー!

ユニオン型との組み合わせで型を生成

テンプレートリテラル型にユニオン型を渡すと、すべての組み合わせが自動的に展開されます。これにより、パターンの網羅的な型定義が簡単にできます。

TypeScript – ユニオン × テンプレートリテラル
// ユニオンの組み合わせ展開
type Color = "red" | "blue" | "green";
type Size = "small" | "medium" | "large";

type ColorSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large"
// | "blue-small" | "blue-medium" | "blue-large"
// | "green-small" | "green-medium" | "green-large"

// イベント名の型安全な定義
type DOMEvent = "click" | "hover" | "focus" | "blur";
type EventHandler = `on${Capitalize<DOMEvent>}`;
// "onClick" | "onHover" | "onFocus" | "onBlur"

// HTTPメソッドとパスの組み合わせ
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = "/users" | "/products" | "/orders";
type Route = `${HttpMethod} ${Endpoint}`;
// "GET /users" | "GET /products" | "GET /orders"
// | "POST /users" | "POST /products" | ...
// 合計12個のリテラル型

// CSSクラス名の型安全な定義(BEM命名規則)
type Block = "button" | "card" | "modal";
type Modifier = "primary" | "secondary" | "disabled";
type BEMClass = Block | `${Block}--${Modifier}`;
// "button" | "card" | "modal"
// | "button--primary" | "button--secondary" | "button--disabled"
// | "card--primary" | ... | "modal--disabled"

注意:ユニオンの組み合わせは掛け算で増えます。例えば3つの型 x 4つの型 = 12個のリテラル型が生成されます。大きなユニオン同士を組み合わせるとコンパイルが遅くなる場合があるため、必要最小限のユニオンに絞ることが重要です。

Uppercase / Lowercase / Capitalize / Uncapitalize

TypeScript には組み込みの文字列操作型が4つ用意されています。これらはコンパイラに直接組み込まれた特別な型(Intrinsic Types)です。

TypeScript – 組み込み文字列操作型
// Uppercase: すべて大文字に変換
type U1 = Uppercase<"hello">;    // "HELLO"
type U2 = Uppercase<"TypeScript">; // "TYPESCRIPT"

// Lowercase: すべて小文字に変換
type L1 = Lowercase<"HELLO">;    // "hello"
type L2 = Lowercase<"TypeScript">; // "typescript"

// Capitalize: 先頭文字だけ大文字に変換
type C1 = Capitalize<"hello">;     // "Hello"
type C2 = Capitalize<"typeScript">; // "TypeScript"

// Uncapitalize: 先頭文字だけ小文字に変換
type UC1 = Uncapitalize<"Hello">;     // "hello"
type UC2 = Uncapitalize<"TypeScript">; // "typeScript"

// ユニオンと組み合わせ
type Events = "click" | "change" | "submit";
type Handlers = `on${Capitalize<Events>}`;
// "onClick" | "onChange" | "onSubmit"

// 環境変数名の型安全な定義
type EnvKey = "database" | "redis" | "api";
type EnvSuffix = "host" | "port" | "url";
type EnvVar = Uppercase<`${EnvKey}_${EnvSuffix}`>;
// "DATABASE_HOST" | "DATABASE_PORT" | "DATABASE_URL"
// | "REDIS_HOST" | "REDIS_PORT" | "REDIS_URL"
// | "API_HOST" | "API_PORT" | "API_URL"
入力出力説明
Uppercase<S>"hello""HELLO"全文字を大文字に
Lowercase<S>"HELLO""hello"全文字を小文字に
Capitalize<S>"hello""Hello"先頭を大文字に
Uncapitalize<S>"Hello""hello"先頭を小文字に

イベント名やCSSプロパティの型安全な定義

テンプレートリテラル型の実力が最も発揮されるのは、文字列ベースのAPIに型安全性を持たせる場面です。イベントシステム、CSSプロパティ、ルーティングなどで強力に活用できます。

TypeScript – テンプレートリテラル型の実務パターン
// 1. CSSプロパティ名のキャメルケース→ケバブケース変換
type CamelToKebab<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Tail extends Uncapitalize<Tail>
      ? `${Lowercase<Head>}${CamelToKebab<Tail>}`
      : `${Lowercase<Head>}-${CamelToKebab<Tail>}`
    : S;

type K1 = CamelToKebab<"fontSize">;        // "font-size"
type K2 = CamelToKebab<"backgroundColor">; // "background-color"
type K3 = CamelToKebab<"borderTopWidth">;  // "border-top-width"

// 2. パスパラメータの型安全な抽出
type ExtractParams<Path extends string> =
  Path extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${infer _Start}:${infer Param}`
      ? Param
      : never;

type Params1 = ExtractParams<"/users/:id">;          // "id"
type Params2 = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

// 3. ドットパスの型安全な定義
type DotPath<T> = T extends object
  ? {
      [K in keyof T & string]:
        | K
        | `${K}.${DotPath<T[K]> & string}`
    }[keyof T & string]
  : never;

interface Config {
  db: {
    host: string;
    port: number;
  };
  api: {
    key: string;
  };
}

type ConfigPaths = DotPath<Config>;
// "db" | "db.host" | "db.port" | "api" | "api.key"

// 4. SQLクエリの型安全なカラム参照
type TableColumns<Tables extends Record<string, string>> = {
  [T in keyof Tables & string]: `${T}.${Tables[T]}`
}[keyof Tables & string];

type DbSchema = {
  users: "id" | "name" | "email";
  posts: "id" | "title" | "author_id";
};

type Columns = TableColumns<DbSchema>;
// "users.id" | "users.name" | "users.email"
// | "posts.id" | "posts.title" | "posts.author_id"

ポイント:テンプレートリテラル型と infer を組み合わせると、文字列パターンの解析も型レベルで行えます。infer で文字列の一部をキャプチャし、再帰的に処理することで複雑な文字列変換が可能です。

インデックスアクセス型(Indexed Access Types)

インデックスアクセス型は、別の型の特定のプロパティの型をルックアップ(参照)する機能です。JavaScriptのオブジェクトにブラケット記法でアクセスするのと同じ感覚で、型レベルでプロパティの型を取得できます。

T[K] での型の取得

T[K] 構文で、型 T のキー K に対応するプロパティの型を取得できます。

TypeScript – インデックスアクセス型の基本
interface User {
  id: number;
  name: string;
  email: string;
  address: {
    street: string;
    city: string;
    zip: string;
  };
  roles: string[];
}

// 単一プロパティの型を取得
type UserId = User["id"];       // number
type UserName = User["name"];   // string
type UserAddr = User["address"]; // { street: string; city: string; zip: string }
type UserRoles = User["roles"];  // string[]

// ユニオン型のキーで複数プロパティの型をまとめて取得
type IdOrName = User["id" | "name"]; // number | string

// keyof と組み合わせてすべてのプロパティの値型を取得
type UserValues = User[keyof User];
// number | string | { street: string; city: string; zip: string } | string[]

// ジェネリクスと組み合わせ
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { /* ... */ } as User;
const name = getProperty(user, "name");     // string
const address = getProperty(user, "address"); // { street: string; ... }

配列要素の型取得 T[number]

配列型に対して T[number] を使うと、配列の要素の型を取得できます。タプル型の場合は、各要素型のユニオンが返されます。

TypeScript – 配列要素の型取得
// 配列型から要素型を取得
type StringArray = string[];
type StringElement = StringArray[number]; // string

// タプル型から要素型を取得
type Tuple = [string, number, boolean];
type TupleElement = Tuple[number]; // string | number | boolean

// タプルの特定位置の型を取得
type First = Tuple[0];  // string
type Second = Tuple[1]; // number
type Third = Tuple[2];  // boolean

// 実務例: as const 配列からユニオン型を生成
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

const STATUS_CODES = [200, 201, 400, 404, 500] as const;
type StatusCode = (typeof STATUS_CODES)[number]; // 200 | 201 | 400 | 404 | 500

// 実務例: 設定オブジェクトから型を生成
const routes = {
  home: "/",
  about: "/about",
  users: "/users",
  userDetail: "/users/:id",
} as const;

type RouteName = keyof typeof routes;
// "home" | "about" | "users" | "userDetail"

type RoutePath = (typeof routes)[RouteName];
// "/" | "/about" | "/users" | "/users/:id"

ネストしたプロパティの型取得

インデックスアクセス型をチェーンすることで、深くネストしたプロパティの型にアクセスできます。

TypeScript – ネストしたプロパティへのアクセス
interface ApiResponse {
  status: number;
  data: {
    user: {
      id: number;
      profile: {
        avatar: string;
        bio: string;
      };
    };
    posts: {
      id: number;
      title: string;
    }[];
  };
}

// ネストしたアクセス
type UserData = ApiResponse["data"]["user"];
// { id: number; profile: { avatar: string; bio: string } }

type Profile = ApiResponse["data"]["user"]["profile"];
// { avatar: string; bio: string }

type Avatar = ApiResponse["data"]["user"]["profile"]["avatar"];
// string

// 配列の要素型もチェーンで取得
type Post = ApiResponse["data"]["posts"][number];
// { id: number; title: string }

type PostTitle = ApiResponse["data"]["posts"][number]["title"];
// string

// 実務例: ドットパスでネストしたプロパティの型を取得するユーティリティ
type GetByPath<T, Path extends string> =
  Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? GetByPath<T[Key], Rest>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;

type G1 = GetByPath<ApiResponse, "data.user.profile.avatar">; // string
type G2 = GetByPath<ApiResponse, "status">;                // number
type G3 = GetByPath<ApiResponse, "data.user">;             // { id: number; profile: { ... } }

インデックスアクセス型のまとめ

  • T["key"] でオブジェクト型のプロパティの型を取得
  • T[number] で配列型の要素の型を取得
  • T[keyof T] ですべてのプロパティの値型のユニオンを取得
  • チェーンして T["a"]["b"]["c"] のようにネストした型にアクセス可能
  • as const と組み合わせると、実行時の値からリテラル型のユニオンを生成できる

条件型 x マップ型 x テンプレートリテラル型の組み合わせ

ここまで学んだ高度な型を組み合わせることで、型レベルのプログラミングが本格的に行えます。実務で役立つ強力な型パターンを見ていきましょう。

DeepPartial, DeepReadonly の実装

TypeScript組み込みの PartialReadonly は1階層のみに作用します。ネストしたオブジェクトにも再帰的に適用する Deep バージョンを実装しましょう。

TypeScript – DeepPartial / DeepReadonly
// DeepPartial: 全階層のプロパティをオプショナルにする
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends any[]
      ? T[K]  // 配列はそのまま
      : DeepPartial<T[K]>  // オブジェクトは再帰
    : T[K]  // プリミティブはそのまま
};

interface AppConfig {
  server: {
    host: string;
    port: number;
    ssl: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    url: string;
    pool: number;
  };
}

// 部分的な設定の上書きに便利
function updateConfig(partial: DeepPartial<AppConfig>) {
  // ...
}

updateConfig({
  server: {
    ssl: { enabled: true }  // cert は省略可能
  }
  // database は省略可能
});

// DeepReadonly: 全階層を読み取り専用にする
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]  // 関数はそのまま
      : DeepReadonly<T[K]>
    : T[K]
};

const config: DeepReadonly<AppConfig> = {
  server: { host: "localhost", port: 3000, ssl: { enabled: false, cert: "" } },
  database: { url: "localhost:5432", pool: 10 }
};

// config.server.port = 8080;  // エラー!readonly
// config.server.ssl.enabled = true;  // エラー!deep readonly

// DeepRequired: 全階層を必須にする
type DeepRequired<T> = {
  [K in keyof T]-?: T[K] extends object
    ? DeepRequired<T[K]>
    : T[K]
};

型安全なイベントシステムの構築

マップ型とテンプレートリテラル型を組み合わせて、型安全なイベントエミッターを構築します。イベント名とペイロードの型が自動的に対応付けされます。

TypeScript – 型安全なイベントシステム
// イベントマップの定義
interface EventMap {
  userLogin: { userId: string; timestamp: Date };
  userLogout: { userId: string };
  pageView: { url: string; referrer?: string };
  error: { code: number; message: string };
}

// イベントハンドラの型を自動生成
type EventHandlerMap<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (payload: T[K]) => void
};

type Handlers = EventHandlerMap<EventMap>;
// {
//   onUserLogin: (payload: { userId: string; timestamp: Date }) => void;
//   onUserLogout: (payload: { userId: string }) => void;
//   onPageView: (payload: { url: string; referrer?: string }) => void;
//   onError: (payload: { code: number; message: string }) => void;
// }

// 型安全なイベントエミッタークラス
class TypedEventEmitter<Events extends Record<string, any>> {
  private listeners = new Map<string, Set<Function>>();

  on<K extends keyof Events & string>(
    event: K,
    handler: (payload: Events[K]) => void
  ): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(handler);
  }

  emit<K extends keyof Events & string>(
    event: K,
    payload: Events[K]
  ): void {
    this.listeners.get(event)?.forEach(fn => fn(payload));
  }
}

// 使用例
const emitter = new TypedEventEmitter<EventMap>();

// イベント名とペイロード型が自動で対応する
emitter.on("userLogin", (payload) => {
  // payload は { userId: string; timestamp: Date } と推論される
  console.log(payload.userId);
});

emitter.emit("error", { code: 404, message: "Not Found" }); // OK
// emitter.emit("error", { code: "404" });  // エラー!code は number

APIレスポンスの型変換

バックエンドのAPIレスポンスをフロントエンドで使いやすい型に自動変換するパターンです。snake_case → camelCase の変換や、日付文字列 → Date オブジェクトへの変換を型レベルで表現します。

TypeScript – API レスポンスの型変換
// snake_case → camelCase の型レベル変換
type SnakeToCamel<S extends string> =
  S extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<SnakeToCamel<Tail>>}`
    : S;

type SC1 = SnakeToCamel<"user_name">;       // "userName"
type SC2 = SnakeToCamel<"created_at">;      // "createdAt"
type SC3 = SnakeToCamel<"is_email_verified">; // "isEmailVerified"

// オブジェクト全体のキーを camelCase に変換
type CamelizeKeys<T> = {
  [K in keyof T as SnakeToCamel<K & string>]: T[K] extends object
    ? T[K] extends any[]
      ? T[K]
      : CamelizeKeys<T[K]>
    : T[K]
};

// APIレスポンス(snake_case)
interface ApiUser {
  user_id: number;
  first_name: string;
  last_name: string;
  email_address: string;
  created_at: string;
  is_active: boolean;
}

// 自動変換結果(camelCase)
type FrontendUser = CamelizeKeys<ApiUser>;
// {
//   userId: number;
//   firstName: string;
//   lastName: string;
//   emailAddress: string;
//   createdAt: string;
//   isActive: boolean;
// }

// 日付フィールドを自動で Date に変換する型
type DateFields<T> = {
  [K in keyof T]: K extends `${string}_at` | `${string}_date`
    ? Date
    : T[K]
};

type WithDates = DateFields<ApiUser>;
// created_at が Date 型になる

// 完全な変換パイプライン
type TransformResponse<T> = CamelizeKeys<DateFields<T>>;

ポイント:型変換パイプラインを構築することで、バックエンドの型定義を1つ書くだけでフロントエンドの型が自動生成されます。APIスキーマが変更された場合も、型エラーが自動で検出されるため保守性が大幅に向上します。

実務パターン集

ここでは、高度な型テクニックを組み合わせた実務で即使えるパターンを紹介します。フォームバリデーション、型安全なルーティング、Builder パターン、型レベルのバリデーションなど、実際のプロジェクトで頻繁に必要になるパターンを網羅します。

フォームバリデーションの型

フォームの各フィールドに対してバリデーションルールを型安全に定義するパターンです。フィールド名とバリデーションルールの対応がコンパイル時に検証されます。

TypeScript – 型安全なフォームバリデーション
// バリデーションルールの型定義
type ValidationRule<T> = {
  required?: boolean;
  validate?: (value: T) => string | true;
} & (T extends string
  ? { minLength?: number; maxLength?: number; pattern?: RegExp }
  : T extends number
  ? { min?: number; max?: number }
  : {});

// フォームスキーマから型安全にバリデーションルールを定義
type FormValidation<T> = {
  [K in keyof T]?: ValidationRule<T[K]>
};

// フォームエラーの型
type FormErrors<T> = {
  [K in keyof T]?: string
};

// 使用例
interface RegisterForm {
  username: string;
  email: string;
  age: number;
  password: string;
}

const validation: FormValidation<RegisterForm> = {
  username: {
    required: true,
    minLength: 3,
    maxLength: 20,
    pattern: /^[a-zA-Z0-9_]+$/,
  },
  email: {
    required: true,
    validate: (v) => v.includes("@") || "有効なメールアドレスを入力してください",
  },
  age: {
    min: 0,
    max: 150,
    // minLength: 3,  // エラー!number型にminLengthは使えない
  },
  password: {
    required: true,
    minLength: 8,
  },
};

型安全なルーティング

URLパスからパラメータを型レベルで自動抽出するルーティングシステムです。パスパラメータの漏れや型の不一致をコンパイル時に検出できます。

TypeScript – 型安全なルーティング
// パスからパラメータ名を抽出
type ExtractRouteParams<Path extends string> =
  Path extends `${infer _}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : Path extends `${infer _}:${infer Param}`
      ? { [K in Param]: string }
      : {};

// パラメータの型を確認
type P1 = ExtractRouteParams<"/users/:id">;
// { id: string }

type P2 = ExtractRouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }

type P3 = ExtractRouteParams<"/about">;
// {} (パラメータなし)

// 型安全なルーター
function createRoute<Path extends string>(
  path: Path,
  handler: (params: ExtractRouteParams<Path>) => void
) {
  // 実装...
}

// パスパラメータが自動推論される
createRoute("/users/:id", (params) => {
  console.log(params.id); // OK: string
  // params.name  // エラー!"name" は存在しない
});

createRoute("/users/:userId/posts/:postId", (params) => {
  console.log(params.userId);  // OK
  console.log(params.postId);  // OK
});

Builder パターンの型付け

Builder パターンでは、メソッドチェーンの各ステップで設定済みのプロパティを型が追跡するように実装できます。必須プロパティがすべて設定されるまで build() を呼べないようにすることも可能です。

TypeScript – 型安全な Builder パターン
// ビルダーの設定状態を型で追跡
interface QueryConfig {
  table: string;
  select: string[];
  where: string;
  orderBy: string;
  limit: number;
}

type RequiredKeys = "table" | "select";

class QueryBuilder<Set extends string = never> {
  private config: Partial<QueryConfig> = {};

  from(table: string): QueryBuilder<Set | "table"> {
    this.config.table = table;
    return this as any;
  }

  columns(...cols: string[]): QueryBuilder<Set | "select"> {
    this.config.select = cols;
    return this as any;
  }

  where(condition: string): QueryBuilder<Set | "where"> {
    this.config.where = condition;
    return this as any;
  }

  // build() は必須プロパティがすべてセットされた場合のみ呼べる
  build(
    this: QueryBuilder<Set & RequiredKeys extends never ? never : Set>
  ): string {
    return `SELECT ...`;
  }
}

// 使用例
const query = new QueryBuilder()
  .from("users")
  .columns("id", "name")
  .where("active = true")
  .build();  // OK: table と select がセット済み

// new QueryBuilder().from("users").build();
// エラー!select がセットされていない

型レベルのバリデーション

条件型を使って、関数に渡される引数が特定の条件を満たすかどうかを型レベルで検証できます。条件を満たさない場合はコンパイルエラーになります。

TypeScript – 型レベルバリデーション
// 文字列が空でないことを保証する型
type NonEmptyString<S extends string> =
  S extends "" ? never : S;

function greet<S extends string>(name: NonEmptyString<S>): string {
  return `Hello, ${name}!`;
}

greet("Alice");  // OK
// greet("");       // エラー!空文字は許可されない

// オブジェクトのキーが重複していないことを保証
type UniqueArray<T extends readonly string[]> =
  T extends readonly [infer First extends string, ...infer Rest extends string[]]
    ? First extends Rest[number]
      ? never  // 重複あり
      : [First, ...UniqueArray<Rest> extends never ? never : Rest]
    : T;

// 型の等価性チェック
type Equals<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

// 型のアサーション(テスト用)
type Assert<T extends true> = T;

// 型テスト: 型が正しいことを検証
type _test1 = Assert<Equals<ReturnType<() => string>, string>>;  // OK
type _test2 = Assert<Equals<Exclude<"a" | "b", "b">, "a">>;      // OK
// type _test3 = Assert<Equals<string, number>>;  // エラー!

// 実務例: API エンドポイントの設定が正しいことを保証
type ValidEndpoint<T extends string> =
  T extends `/${string}` ? T : never;

function registerEndpoint<T extends string>(
  path: ValidEndpoint<T>
) { /* ... */ }

registerEndpoint("/api/users");   // OK
// registerEndpoint("api/users");  // エラー!/ で始まらない

実務パターンのまとめ

  • フォームバリデーション: 条件型でフィールドの型に応じたルールを制限
  • ルーティング: テンプレートリテラル型 + infer でパスパラメータを自動抽出
  • Builder パターン: 型パラメータで設定状態を追跡し、必須項目の漏れを防止
  • 型バリデーション: 条件型とテンプレートリテラル型で入力パターンを制限

パフォーマンスと制限

高度な型はとても強力ですが、使い方を誤るとコンパイル速度の低下やエラーメッセージの難読化を招くことがあります。ここでは再帰型の制限型の複雑さとコンパイル速度デバッグテクニックを解説します。

再帰型の深度制限

TypeScriptの再帰型にはインスタンス化の深度制限があります。TypeScript 4.5以降では尾再帰(tail-recursive)の最適化が導入されましたが、それでも注意が必要です。

TypeScript – 再帰型の制限と対策
// 危険: 深い再帰は "Type instantiation is excessively deep" エラーになる
type DeepNested<T, Depth extends number> =
  Depth extends 0 ? T : { nested: DeepNested<T, /* Depth - 1 */> };
// TypeScriptでは型レベルの算術は直接できない

// 対策1: タプルを使ったカウンタ
type BuildTuple<N extends number, T extends any[] = []> =
  T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;

type Length3 = BuildTuple<3>; // [unknown, unknown, unknown]

// 対策2: 再帰の深さを制限する
type MaxDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

type SafeDeepPartial<T, D extends number = 5> = [D] extends [never]
  ? T  // 深さ上限に達したらそのまま返す
  : {
      [K in keyof T]?: T[K] extends object
        ? SafeDeepPartial<T[K], MaxDepth[D]>
        : T[K]
    };

// 対策3: ts-toolbelt などのライブラリを活用
// npm install ts-toolbelt で型レベルの数値演算が可能に

注意:TypeScript の再帰型の深度制限は、通常の型のインスタンス化で約50レベル、条件型の再帰で約1000レベルです(バージョンにより異なります)。実務では5〜10レベル程度の再帰に収めるのが安全です。

型の複雑さとコンパイル速度

複雑な型計算はコンパイル時間に直接影響します。特にユニオン型の組み合わせ爆発や、深い再帰型は要注意です。

TypeScript – コンパイル速度の最適化
// 悪い例: ユニオンの組み合わせ爆発
type BigUnion = "a" | "b" | "c" | ... | "z"; // 26要素
type Explosion = `${BigUnion}-${BigUnion}-${BigUnion}`;
// 26 x 26 x 26 = 17,576通りのリテラル型!非常に遅くなる

// 良い例: 必要な組み合わせだけを定義
type ValidCombination =
  | "a-b-c"
  | "x-y-z"
  | "d-e-f";

// パフォーマンス計測: --generateTrace オプション
// tsc --generateTrace traceDir
// Chrome DevTools の Performance タブで trace.json を分析

// キャッシュを意識した型設計
// 悪い例: 毎回展開される
type BadPattern<T> = {
  [K in keyof T]: T[K] extends object ? SomeComplexTransform<T[K]> : T[K]
};

// 良い例: 中間型を使ってキャッシュ
type CachedTransform<T> = SomeComplexTransform<T>;
type GoodPattern<T> = {
  [K in keyof T]: T[K] extends object ? CachedTransform<T[K]> : T[K]
};
アンチパターン問題対策
大きなユニオンの組み合わせ型の数が指数的に増加組み合わせを手動で限定する
深い再帰型深度制限エラーまたは遅延深さを制限、尾再帰で最適化
過度に複雑な条件型のネストエラーメッセージが読めない中間型に分解して名前を付ける
分配条件型の意図しない分配予想外の型が生成される[T] extends [U] で分配を防止

デバッグテクニック

複雑な型のデバッグは難しいですが、いくつかのテクニックを使うことで型の中間結果を確認したり、型エラーの原因を特定できます。

TypeScript – 型のデバッグテクニック
// 1. 型の「展開」ユーティリティ(ホバーで中身を確認)
type Prettify<T> = {
  [K in keyof T]: T[K]
} & {};

// Prettify なし: ホバーすると Pick<User, "name" | "email"> と表示
// Prettify あり: ホバーすると { name: string; email: string } と展開表示
type PickedUser = Prettify<Pick<User, "name" | "email">>;

// 2. 型のステップ分割
// 複雑な型を段階的に分解して、各ステップをホバーで確認
type Step1 = keyof User;            // "id" | "name" | ...
type Step2 = Exclude<Step1, "id">;  // "name" | ...
type Step3 = Pick<User, Step2>;      // { name: ...; ... }

// 3. // @ts-expect-error でネガティブテスト
// @ts-expect-error: number は string に割り当てられない
const bad: string = 42;

// 4. 型のログ出力(コンパイラがエラーメッセージで型を表示)
type Debug<T> = { [K in keyof T]: T[K] };
// 意図的にエラーを起こして型を確認
// const _debug: Debug<SomeComplexType> = null as any as number;
// エラーメッセージに展開された型が表示される

// 5. tsc --noEmit で型チェックだけ実行
// npx tsc --noEmit --extendedDiagnostics
// 型チェックの時間やメモリ使用量を確認できる

ポイント:Prettify ユーティリティ型は、VS Code でのホバー表示を改善する非常に便利なテクニックです。複雑な型のインターセクションや Pick/Omit の結果を展開して読みやすくしてくれます。すべてのプロジェクトに入れておくことをおすすめします。

TypeScript 組み込みユーティリティ型の完全リファレンス

TypeScript には、高度な型機能を活用した組み込みユーティリティ型が多数用意されています。ここでは各ユーティリティ型の実装原理と使い方をまとめます。

TypeScript – 組み込みユーティリティ型の実装
// --- マップ型ベース ---

// Partial<T>: すべてのプロパティをオプショナルに
type Partial<T> = { [P in keyof T]?: T[P] };

// Required<T>: すべてのプロパティを必須に
type Required<T> = { [P in keyof T]-?: T[P] };

// Readonly<T>: すべてのプロパティを読み取り専用に
type Readonly<T> = { readonly [P in keyof T]: T[P] };

// Pick<T, K>: 指定したキーのみを抽出
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// Record<K, V>: キーと値の型を指定してオブジェクトを生成
type Record<K extends string | number | symbol, T> = { [P in K]: T };

// --- 条件型ベース ---

// Exclude<T, U>: ユニオンから除外
type Exclude<T, U> = T extends U ? never : T;

// Extract<T, U>: ユニオンから抽出
type Extract<T, U> = T extends U ? T : never;

// NonNullable<T>: null と undefined を除外
type NonNullable<T> = T & {};

// --- infer ベース ---

// ReturnType<T>: 関数の戻り値型
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

// Parameters<T>: 関数のパラメータ型
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

// ConstructorParameters<T>: コンストラクタのパラメータ型
type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

// InstanceType<T>: コンストラクタのインスタンス型
type InstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : any;

// --- マップ型 + 条件型 の組み合わせ ---

// Omit<T, K>: 指定したキーを除外
type Omit<T, K extends string | number | symbol> =
  { [P in Exclude<keyof T, K>]: T[P] };
カテゴリユーティリティ型使用する高度な型
オブジェクト変換Partial, Required, Readonlyマップ型 + 修飾子
キー操作Pick, Omit, Recordマップ型 + 条件型
ユニオン操作Exclude, Extract, NonNullable分配条件型
関数型ReturnType, Parameters条件型 + infer
クラス型InstanceType, ConstructorParameters条件型 + infer
文字列操作Uppercase, Lowercase, Capitalizeテンプレートリテラル型
PromiseAwaited再帰的条件型 + infer

高度な型の実践例: 型安全な状態管理

最後に、ここまで学んだすべての高度な型テクニックを総合的に活用した実践例を紹介します。React/Vue 等のフロントエンドフレームワークでよく使われる型安全な状態管理ストアの型定義です。

TypeScript – 型安全な状態管理ストアの完全実装
// ストアの定義型
interface StoreDefinition<S, G, A> {
  state: () => S;
  getters?: G & ThisType<S & GetterReturnTypes<G>>;
  actions?: A & ThisType<S & GetterReturnTypes<G> & A>;
}

// ゲッターの戻り値型を抽出
type GetterReturnTypes<G> = {
  readonly [K in keyof G]: G[K] extends (...args: any[]) => infer R ? R : never
};

// ストアの型を統合
type Store<S, G, A> = S & GetterReturnTypes<G> & A & {
  $patch: (partial: DeepPartial<S>) => void;
  $subscribe: <K extends keyof S>(
    key: K,
    callback: (newVal: S[K], oldVal: S[K]) => void
  ) => () => void;
};

// defineStore 関数
function defineStore<S, G, A>(
  definition: StoreDefinition<S, G, A>
): Store<S, G, A> {
  // 実装...
  return {} as any;
}

// 使用例: すべての型が自動推論される
const store = defineStore({
  state: () => ({
    count: 0,
    name: "TypeScript",
    items: [] as string[],
  }),

  getters: {
    doubleCount() { return this.count * 2; },
    itemCount() { return this.items.length; },
    summary() { return `${this.name}: ${this.doubleCount}`; },
  },

  actions: {
    increment() { this.count++; },
    addItem(item: string) { this.items.push(item); },
    async fetchItems() {
      const res = await fetch("/api/items");
      this.items = await res.json();
    },
  },
});

// store の型: すべて自動推論される
store.count;        // number
store.doubleCount;  // number(getter)
store.increment();  // void(action)
store.addItem("hello"); // void

// $subscribe で変更を監視(キーが型安全)
store.$subscribe("count", (newVal, oldVal) => {
  // newVal, oldVal は number と推論される
  console.log(`count: ${oldVal} → ${newVal}`);
});

TypeScript 高度な型チートシート

この記事で解説した高度な型機能をチートシートとしてまとめます。実務で迷ったときにすぐ参照できるようにしておきましょう。

TypeScript – 高度な型チートシート
// ═══════════════════════════════════════
// 条件型(Conditional Types)
// ═══════════════════════════════════════

// 基本形
type If<C extends boolean, T, F> = C extends true ? T : F;

// 型の抽出
type Unwrap<T> = T extends Promise<infer U> ? U : T;

// 分配防止
type NoDist<T, U> = [T] extends [U] ? true : false;

// ═══════════════════════════════════════
// マップ型(Mapped Types)
// ═══════════════════════════════════════

// 基本変換
type Transform<T> = { [K in keyof T]: NewType };

// 修飾子の追加/削除
type AddReadonly<T> = { readonly [K in keyof T]: T[K] };
type RemoveOptional<T> = { [K in keyof T]-?: T[K] };

// キーリマッピング
type Rename<T> = { [K in keyof T as `new_${string & K}`]: T[K] };

// キーフィルタリング
type Filter<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] };

// ═══════════════════════════════════════
// テンプレートリテラル型
// ═══════════════════════════════════════

// パターンマッチ
type Match = `${"GET"|"POST"} /${string}`;

// 文字列解析
type Parse<S> = S extends `${infer A}.${infer B}` ? [A, B] : [S];

// ═══════════════════════════════════════
// よく使うパターン
// ═══════════════════════════════════════

// 型の展開(デバッグ用)
type Prettify<T> = { [K in keyof T]: T[K] } & {};

// 値の型からユニオンを生成
type ValueOf<T> = T[keyof T];

// 配列からユニオン
type ArrayToUnion<T extends readonly any[]> = T[number];

まとめ

TypeScriptの高度な型は、最初は複雑に感じるかもしれませんが、基本的なパターンを理解すればその組み合わせで多くのことが実現できます。この記事で解説した内容を振り返りましょう。

この記事のまとめ

  • 条件型T extends U ? X : Y)で型レベルの条件分岐ができる
  • infer キーワードで型パターンから部分型を抽出できる
  • 分配条件型はユニオンの各メンバーに対して個別に評価される([T] で防止可能)
  • マップ型{[K in keyof T]: ...})でオブジェクト型を一括変換できる
  • 修飾子(readonly, ?)の追加(+)と削除(-)が可能
  • as 句でキーの名前を変換・フィルタリングできる
  • テンプレートリテラル型で文字列パターンを型として表現できる
  • ユニオンとの組み合わせで全パターンが自動生成される
  • インデックスアクセス型T[K], T[number])でネストした型を取得できる
  • これらを組み合わせて DeepPartial、イベントシステム、APIレスポンス変換などの実務パターンが構築できる
  • 再帰型の深度制限やコンパイル速度に注意し、Prettify でデバッグする

高度な型を使いこなすためのステップとしては、まず条件型と infer を徹底的に練習し、次にマップ型でオブジェクトの変換パターンを習得、最後にテンプレートリテラル型で文字列パターンの型安全性を学ぶ、という順序がおすすめです。

TypeScriptの型システムはチューリング完全であり、理論上はあらゆる計算を型レベルで行えます。しかし実務では、コードの可読性とコンパイル速度のバランスを取ることが重要です。型を書く目的は、ランタイムエラーを防ぎ、開発者体験を向上させることです。過度に複雑な型よりも、チームメンバーが理解できるシンプルな型を心がけましょう。

次のステップ:この記事の知識を定着させるために、実際のプロジェクトで DeepPartialPrettify を使ってみてください。また、TypeScript の公式ドキュメントや type-challenges リポジトリで型パズルに挑戦することで、型プログラミングのスキルが飛躍的に向上します。