最終更新:

TypeScript Generics上級テクニック: 条件型・マッピング型・テンプレートリテラル型


TypeScriptのGenericsは、単なる型パラメータ以上の力を持っています。この記事では、条件型マッピング型テンプレートリテラル型を組み合わせた高度なパターンを解説します。

条件型(Conditional Types)の基礎

基本構文

// T extends U ? X : Y
// T が U に割り当て可能なら X、そうでなければ Y

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

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

実用例: NonNullable の実装

// undefined | null を除外
type MyNonNullable<T> = T extends null | undefined ? never : T;

type Example1 = MyNonNullable<string | null>;      // string
type Example2 = MyNonNullable<number | undefined>; // number
type Example3 = MyNonNullable<boolean | null | undefined>; // boolean

配列型の抽出

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

type Str = ArrayElement<string[]>;        // string
type Num = ArrayElement<number[]>;        // number
type Mixed = ArrayElement<(string | number)[]>; // string | number
type NotArray = ArrayElement<string>;     // never

// Readonly配列にも対応
type ReadonlyArrayElement<T> = T extends readonly (infer U)[] ? U : never;

type ReadonlyStr = ReadonlyArrayElement<readonly string[]>; // string

Promise の型抽出

// Promiseの解決型を取得
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A = Awaited<Promise<string>>;  // string
type B = Awaited<Promise<number>>;  // number
type C = Awaited<string>;           // string(Promiseでない場合はそのまま)

// ネストしたPromiseに対応
type DeepAwaited<T> = T extends Promise<infer U>
  ? DeepAwaited<U>
  : T;

type Nested = DeepAwaited<Promise<Promise<Promise<string>>>>; // string

関数の戻り値型を取得

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

function getUser() {
  return { id: 1, name: 'Alice' };
}

type User = MyReturnType<typeof getUser>; // { id: number; name: string; }

// async関数の場合
async function fetchData() {
  return { data: 'hello' };
}

type FetchResult = MyReturnType<typeof fetchData>; // Promise<{ data: string; }>
type UnwrappedResult = Awaited<MyReturnType<typeof fetchData>>; // { data: string; }

関数の引数型を取得

// 関数の引数型を抽出
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number, active: boolean) {
  return { name, age, active };
}

type CreateUserParams = MyParameters<typeof createUser>; // [string, number, boolean]

// 特定の引数だけ取得
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

type FirstParam = FirstParameter<typeof createUser>; // string

高度な条件型パターン

ユニオン型の分配(Distributive Conditional Types)

// 条件型はユニオン型に対して分配される
type ToArray<T> = T extends any ? T[] : never;

type StringOrNumberArray = ToArray<string | number>;
// string[] | number[] ( (string | number)[] ではない)

// 分配を防ぐには配列で包む
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Combined = ToArrayNonDistributive<string | number>;
// (string | number)[]

型の除外(Exclude)

// ユニオン型から特定の型を除外
type MyExclude<T, U> = T extends U ? never : T;

type A = MyExclude<'a' | 'b' | 'c', 'a'>;       // 'b' | 'c'
type B = MyExclude<string | number, string>;    // number
type C = MyExclude<string | number | boolean, string | boolean>; // number

// 実用例: イベントハンドラーの型を除外
type DOMEventNames = keyof HTMLElementEventMap;
type CustomEvents = 'custom:load' | 'custom:save';

type AllEvents = DOMEventNames | CustomEvents;
type OnlyCustom = MyExclude<AllEvents, DOMEventNames>; // 'custom:load' | 'custom:save'

型の抽出(Extract)

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

type A = MyExtract<'a' | 'b' | 'c', 'a' | 'c'>; // 'a' | 'c'
type B = MyExtract<string | number, number>;    // number

// 実用例: 特定の形状のオブジェクトのみ抽出
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; size: number }
  | { kind: 'triangle'; base: number; height: number };

type CircleOrSquare = MyExtract<Shape, { kind: 'circle' | 'square' }>;
// { kind: 'circle'; radius: number } | { kind: 'square'; size: number }

マッピング型(Mapped Types)

基本的なマッピング型

// オブジェクトの全プロパティをオプションにする
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

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

type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string; }

// オブジェクトの全プロパティを必須にする
type MyRequired<T> = {
  [P in keyof T]-?: T[P];
};

type RequiredUser = MyRequired<PartialUser>;
// { id: number; name: string; email: string; }

Readonly と Mutable

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

type ReadonlyUser = MyReadonly<User>;
// { readonly id: number; readonly name: string; readonly email: string; }

// readonlyを解除する
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string; email: string; }

プロパティの型を変換

// 全プロパティをnullableにする
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

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

// 全プロパティをPromiseでラップ
type Promisify<T> = {
  [P in keyof T]: Promise<T[P]>;
};

type AsyncUser = Promisify<User>;
// { id: Promise<number>; name: Promise<string>; email: Promise<string>; }

特定のプロパティのみ抽出

// 特定の型のプロパティのみ抽出
type PickByType<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

interface Person {
  name: string;
  age: number;
  isActive: boolean;
  createdAt: Date;
  metadata: Record<string, any>;
}

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

type NumberProps = PickByType<Person, number>;
// { age: number; }

// 関数プロパティのみ抽出
type Methods<T> = PickByType<T, Function>;

interface UserService {
  name: string;
  getUser: (id: number) => Promise<User>;
  updateUser: (user: User) => Promise<void>;
  count: number;
}

type UserMethods = Methods<UserService>;
// { getUser: (id: number) => Promise<User>; updateUser: (user: User) => Promise<void>; }

プロパティ名の変換

// プロパティ名にプレフィックスを追加
type AddPrefix<T, Prefix extends string> = {
  [P in keyof T as `${Prefix}${string & P}`]: T[P];
};

type PrefixedUser = AddPrefix<User, 'get'>;
// { getId: number; getName: string; getEmail: string; }

// Getterを生成
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }

// Setterを生成
type Setters<T> = {
  [P in keyof T as `set${Capitalize<string & P>}`]: (value: T[P]) => void;
};

type UserSetters = Setters<User>;
// { setId: (value: number) => void; setName: (value: string) => void; setEmail: (value: string) => void; }

テンプレートリテラル型

基本的な文字列操作

// 文字列リテラルの結合
type Greeting = `Hello, ${string}!`;

const greet1: Greeting = 'Hello, World!';  // ✅
const greet2: Greeting = 'Hello, Alice!';  // ✅
// const greet3: Greeting = 'Hi, Bob!';    // ❌ エラー

// 複数の型を組み合わせ
type Protocol = 'http' | 'https';
type Domain = string;
type URL = `${Protocol}://${Domain}`;

const url1: URL = 'https://example.com';  // ✅
const url2: URL = 'http://localhost';     // ✅
// const url3: URL = 'ftp://example.com'; // ❌ エラー

ルーティングパスの型安全化

// APIエンドポイントの型定義
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'posts' | 'comments';
type Endpoint = `/${Resource}` | `/${Resource}/${string}`;

const endpoint1: Endpoint = '/users';       // ✅
const endpoint2: Endpoint = '/posts/123';   // ✅
// const endpoint3: Endpoint = '/invalid';  // ❌

// RESTful APIの型
type RestEndpoint<R extends string, ID extends string | number = string> =
  | `/${R}`
  | `/${R}/${ID}`;

type UserEndpoint = RestEndpoint<'users', number>;
// '/users' | '/users/${number}'

// ネストしたリソース
type NestedEndpoint<
  R1 extends string,
  R2 extends string,
  ID1 extends string | number = string,
  ID2 extends string | number = string
> = `/${R1}/${ID1}/${R2}` | `/${R1}/${ID1}/${R2}/${ID2}`;

type PostCommentEndpoint = NestedEndpoint<'posts', 'comments', number, number>;
// '/posts/${number}/comments' | '/posts/${number}/comments/${number}'

CSSプロパティの型安全化

// CSS単位
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue<T extends number | string = number> = `${T}${CSSUnit}`;

const width1: CSSValue = '100px';   // ✅
const width2: CSSValue = '50%';     // ✅
const width3: CSSValue<16> = '16rem'; // ✅

// CSSプロパティ
type CSSColor = `#${string}` | `rgb(${number}, ${number}, ${number})` | `rgba(${number}, ${number}, ${number}, ${number})`;

const color1: CSSColor = '#ffffff';           // ✅
const color2: CSSColor = 'rgb(255, 255, 255)'; // ✅
const color3: CSSColor = 'rgba(0, 0, 0, 0.5)'; // ✅

// Flexboxプロパティ
type FlexDirection = 'row' | 'column' | 'row-reverse' | 'column-reverse';
type JustifyContent = 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around';

interface FlexContainer {
  display: 'flex';
  flexDirection?: FlexDirection;
  justifyContent?: JustifyContent;
}

イベントハンドラーの型生成

// イベント名からハンドラー名を生成
type EventName = 'click' | 'focus' | 'blur' | 'submit';
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;

type ClickHandler = EventHandler<'click'>;   // 'onClick'
type FocusHandler = EventHandler<'focus'>;   // 'onFocus'

// イベントハンドラーの型を自動生成
type EventHandlers<T extends string> = {
  [E in T as `on${Capitalize<E>}`]: (event: Event) => void;
};

type FormEventHandlers = EventHandlers<'submit' | 'reset' | 'change'>;
// { onSubmit: (event: Event) => void; onReset: (event: Event) => void; onChange: (event: Event) => void; }

高度なGenericsパターン

ファクトリーパターン

// 型安全なファクトリー関数
interface EntityFactory<T> {
  create(props: Omit<T, 'id' | 'createdAt'>): T;
}

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

class UserFactory implements EntityFactory<User> {
  private idCounter = 1;

  create(props: Omit<User, 'id' | 'createdAt'>): User {
    return {
      id: this.idCounter++,
      ...props,
      createdAt: new Date(),
    };
  }
}

const factory = new UserFactory();
const user = factory.create({ name: 'Alice', email: 'alice@example.com' });
// { id: 1, name: 'Alice', email: 'alice@example.com', createdAt: Date }

ビルダーパターン

// 型安全なビルダー
class QueryBuilder<T, Required extends keyof T = never> {
  private filters: Partial<T> = {};

  where<K extends keyof T>(key: K, value: T[K]): QueryBuilder<T, Required | K> {
    this.filters[key] = value;
    return this as any;
  }

  build(this: QueryBuilder<T, keyof T>): Required<T> {
    return this.filters as Required<T>;
  }
}

interface UserQuery {
  name?: string;
  email?: string;
  age?: number;
}

const query = new QueryBuilder<UserQuery>()
  .where('name', 'Alice')
  .where('email', 'alice@example.com')
  .build(); // ✅ 全プロパティが設定済み

// const incomplete = new QueryBuilder<UserQuery>()
//   .where('name', 'Alice')
//   .build(); // ❌ エラー: emailとageが未設定

Zodスキーマからの型推論

import { z } from 'zod';

// スキーマ定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0).max(150),
  isActive: z.boolean().default(true),
});

// スキーマから型を自動生成
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; email: string; age: number; isActive: boolean; }

// API レスポンスの検証
async function getUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  // ランタイム検証 + 型安全
  return UserSchema.parse(data);
}

再帰的な型定義

// JSONの型定義
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// ネストしたオブジェクトを平坦化
type FlattenObject<T> = T extends object
  ? T extends (infer U)[]
    ? U
    : { [K in keyof T]: FlattenObject<T[K]> }
  : T;

interface NestedUser {
  profile: {
    personal: {
      name: string;
      age: number;
    };
    contact: {
      email: string;
    };
  };
  settings: {
    theme: string;
  };
}

type Flat = FlattenObject<NestedUser>;
// { profile: { personal: { name: string; age: number; }; contact: { email: string; }; }; settings: { theme: string; }; }

DeepReadonly と DeepPartial

// 再帰的にReadonlyにする
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? DeepReadonly<T[P]>
    : T[P];
};

// 再帰的にPartialにする
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  api: {
    endpoint: string;
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// 全てのプロパティがreadonlyに

type PartialConfig = DeepPartial<Config>;
// 全てのプロパティがoptionalに

実践例: 型安全なフォームバリデーション

// フィールドの検証ルール
type ValidationRule<T> =
  | { type: 'required'; message?: string }
  | { type: 'minLength'; value: number; message?: string }
  | { type: 'maxLength'; value: number; message?: string }
  | { type: 'pattern'; value: RegExp; message?: string }
  | { type: 'custom'; validate: (value: T) => boolean; message?: string };

// フォームスキーマ
type FormSchema<T> = {
  [P in keyof T]: ValidationRule<T[P]>[];
};

// バリデーション結果
type ValidationErrors<T> = {
  [P in keyof T]?: string[];
};

// フォームバリデーター
class FormValidator<T extends Record<string, any>> {
  constructor(private schema: FormSchema<T>) {}

  validate(data: T): ValidationErrors<T> {
    const errors: ValidationErrors<T> = {};

    for (const [field, rules] of Object.entries(this.schema)) {
      const value = data[field as keyof T];
      const fieldErrors: string[] = [];

      for (const rule of rules as ValidationRule<any>[]) {
        if (rule.type === 'required' && !value) {
          fieldErrors.push(rule.message || `${field} is required`);
        }

        if (rule.type === 'minLength' && typeof value === 'string') {
          if (value.length < rule.value) {
            fieldErrors.push(rule.message || `${field} must be at least ${rule.value} characters`);
          }
        }

        if (rule.type === 'pattern' && typeof value === 'string') {
          if (!rule.value.test(value)) {
            fieldErrors.push(rule.message || `${field} format is invalid`);
          }
        }

        if (rule.type === 'custom') {
          if (!rule.validate(value)) {
            fieldErrors.push(rule.message || `${field} is invalid`);
          }
        }
      }

      if (fieldErrors.length > 0) {
        errors[field as keyof T] = fieldErrors;
      }
    }

    return errors;
  }
}

// 使用例
interface SignupForm {
  email: string;
  password: string;
  confirmPassword: string;
  age: number;
}

const signupSchema: FormSchema<SignupForm> = {
  email: [
    { type: 'required' },
    { type: 'pattern', value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Invalid email format' },
  ],
  password: [
    { type: 'required' },
    { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' },
  ],
  confirmPassword: [
    { type: 'required' },
    {
      type: 'custom',
      validate: (value) => value === formData.password,
      message: 'Passwords do not match',
    },
  ],
  age: [
    { type: 'required' },
    { type: 'custom', validate: (value) => value >= 18, message: 'Must be 18 or older' },
  ],
};

const validator = new FormValidator(signupSchema);
const formData: SignupForm = {
  email: 'test@example.com',
  password: 'short',
  confirmPassword: 'different',
  age: 17,
};

const errors = validator.validate(formData);
// { password: ['Password must be at least 8 characters'], confirmPassword: ['Passwords do not match'], age: ['Must be 18 or older'] }

まとめ

TypeScript Genericsの高度なテクニックを紹介しました。

重要なパターン

  1. 条件型: T extends U ? X : Y による型の分岐
  2. infer: 型パラメータの推論
  3. マッピング型: [P in keyof T] による型の変換
  4. テンプレートリテラル型: 文字列リテラルの型安全な操作
  5. 再帰的型: ネストした構造の型定義

これらのテクニックを組み合わせることで、実行時エラーをコンパイル時に検出できる型安全なコードを書くことができます。

参考リンク