Engineer Quiz

← 記事一覧

TypeScript型システム完全ガイド - Utility Types, Template Literals, infer

TypeScript型システム完全ガイド - Utility Types, Template Literals, infer

📚 概要

TypeScriptの型システムは、JavaScriptに静的型チェックをもたらすだけでなく、高度な型操作を可能にする強力な機能を提供しています。

この記事では、Utility Types、Template Literal Types、infer キーワードなど、TypeScriptの高度な型システムを詳しく解説します。

🕰️ 歴史的背景

TypeScriptの誕生 - 2012年

  • 開発元: Microsoft(Anders Hejlsberg が主導)
  • 背景: 大規模JavaScriptアプリケーションの保守性向上
  • 初期バージョン: 基本的な型アノテーションのみ

型システムの進化

  • TypeScript 2.1 (2016年): Mapped Types 導入
  • TypeScript 2.8 (2018年): Conditional Types, infer 導入
  • TypeScript 4.1 (2020年): Template Literal Types 導入
  • TypeScript 4.9 (2022年): satisfies オペレータ導入

これらの機能により、TypeScriptは世界で最も表現力のある型システムの1つになりました。

🔧 技術解説

1. Utility Types - 組み込み型変換

TypeScriptには、既存の型を変換するための組み込みUtility Typesが多数用意されています。

Partial - すべてのプロパティをオプションにする

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

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

// 使用例: 部分的な更新
function updateUser(id: number, updates: Partial<User>) {
  // updatesは一部のプロパティのみでOK
}

updateUser(1, { name: "Alice" }); // ✅ OK
updateUser(2, { email: "bob@example.com" }); // ✅ OK

Required - すべてのプロパティを必須にする

interface Config {
  host?: string;
  port?: number;
  ssl?: boolean;
}

type RequiredConfig = Required<Config>;
// 結果:
// {
//   host: string;
//   port: number;
//   ssl: boolean;
// }

function validateConfig(config: RequiredConfig) {
  // すべてのプロパティが必須
}

Readonly - すべてのプロパティを読み取り専用にする

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = Readonly<Point>;
// 結果:
// {
//   readonly x: number;
//   readonly y: number;
// }

const point: ReadonlyPoint = { x: 10, y: 20 };
point.x = 30; // ❌ エラー: Cannot assign to 'x' because it is a read-only property

Pick<T, K> - 特定のプロパティのみを選択

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

type UserPublicInfo = Pick<User, 'id' | 'name' | 'email'>;
// 結果:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// APIレスポンスで使用
function getPublicProfile(userId: number): UserPublicInfo {
  // passwordやcreatedAtは含まれない
  return {
    id: 1,
    name: "Alice",
    email: "alice@example.com"
  };
}

Omit<T, K> - 特定のプロパティを除外

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

type UserWithoutPassword = Omit<User, 'password'>;
// 結果:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// フロントエンドで使用
function displayUser(user: UserWithoutPassword) {
  // passwordは存在しない
  console.log(user.name, user.email);
}

Record<K, T> - キーと値の型を指定してオブジェクト型を生成

type Role = 'admin' | 'user' | 'guest';

interface Permission {
  read: boolean;
  write: boolean;
  delete: boolean;
}

type RolePermissions = Record<Role, Permission>;
// 結果:
// {
//   admin: Permission;
//   user: Permission;
//   guest: Permission;
// }

const permissions: RolePermissions = {
  admin: { read: true, write: true, delete: true },
  user: { read: true, write: true, delete: false },
  guest: { read: true, write: false, delete: false }
};

2. Mapped Types - 型を動的に変換

Mapped Typesは、既存の型のプロパティを走査して新しい型を生成します。

// 基本的なMapped Type
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

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

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

実践例: APIレスポンスの型変換

// APIから返されるデータは全てstringの可能性がある
type APIResponse<T> = {
  [P in keyof T]: string;
};

// パース後の型
type ParsedResponse<T> = {
  [P in keyof T]: T[P];
};

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

// APIレスポンス
const rawData: APIResponse<UserData> = {
  id: "123",
  name: "Alice",
  age: "30"
};

// パース
const parsedData: ParsedResponse<UserData> = {
  id: parseInt(rawData.id),
  name: rawData.name,
  age: parseInt(rawData.age)
};

3. Conditional Types - 条件分岐する型

Conditional Typesは、型レベルでif-elseのような条件分岐を実現します。

T extends 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

実践例: 非null型の抽出

type NonNullable<T> = T extends null | undefined ? never : T;

type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<string | null | undefined>; // string

4. infer キーワード - 型を推論して抽出

inferは、Conditional Types内で型を推論して変数に格納します。

関数の戻り値の型を抽出

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

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

配列の要素の型を抽出

type ArrayElement<T> = T extends (infer E)[] ? E : never;

type A = ArrayElement<string[]>;  // string
type B = ArrayElement<number[]>;  // number
type C = ArrayElement<User[]>;    // User

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 A = DeepAwaited<Promise<Promise<string>>>;  // string
type B = DeepAwaited<Promise<Promise<Promise<number>>>>; // number

5. Template Literal Types - 文字列リテラル型の操作

Template Literal Typesは、文字列リテラル型を動的に生成します。

基本例

type World = "world";
type Greeting = `hello ${World}`;  // "hello world"

実践例: イベント名の自動生成

type EventName = 'click' | 'focus' | 'blur';
type EventHandler<T extends string> = `on${Capitalize<T>}`;

type ClickHandler = EventHandler<'click'>;  // "onClick"
type FocusHandler = EventHandler<'focus'>;  // "onFocus"
type BlurHandler = EventHandler<'blur'>;    // "onBlur"

// 全イベントハンドラーの型を生成
type AllEventHandlers = EventHandler<EventName>;
// "onClick" | "onFocus" | "onBlur"

実践例: REST APIのエンドポイント型

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'posts' | 'comments';

type Endpoint = `${Uppercase<HTTPMethod>} /${Resource}`;
// "GET /users" | "GET /posts" | "GET /comments" |
// "POST /users" | "POST /posts" | ...

function request(endpoint: Endpoint) {
  // エンドポイントが型安全
}

request("GET /users");     // ✅ OK
request("POST /comments"); // ✅ OK
request("PATCH /users");   // ❌ エラー: PATCH は存在しない

組み込みString Utility Types

// Uppercase<S> - 大文字に変換
type A = Uppercase<"hello">;  // "HELLO"

// Lowercase<S> - 小文字に変換
type B = Lowercase<"HELLO">;  // "hello"

// Capitalize<S> - 先頭を大文字に
type C = Capitalize<"hello">; // "Hello"

// Uncapitalize<S> - 先頭を小文字に
type D = Uncapitalize<"Hello">; // "hello"

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

// フォームフィールドの定義
interface UserForm {
  email: string;
  password: string;
  age: number;
}

// エラーメッセージの型(各フィールドに対応)
type FormErrors<T> = {
  [K in keyof T]?: string;
};

// バリデーション関数の型
type Validator<T> = (value: T) => string | undefined;

// 各フィールドのバリデータマップ
type ValidatorMap<T> = {
  [K in keyof T]: Validator<T[K]>;
};

// 実装
const validators: ValidatorMap<UserForm> = {
  email: (value: string) => {
    if (!value.includes('@')) return 'Invalid email';
  },
  password: (value: string) => {
    if (value.length < 8) return 'Password too short';
  },
  age: (value: number) => {
    if (value < 18) return 'Must be 18 or older';
  }
};

// バリデーション実行
function validate<T>(data: T, validators: ValidatorMap<T>): FormErrors<T> {
  const errors: FormErrors<T> = {};
  
  for (const key in validators) {
    const error = validators[key](data[key]);
    if (error) {
      errors[key] = error;
    }
  }
  
  return errors;
}

// 使用例
const formData: UserForm = {
  email: "alice",
  password: "12345",
  age: 17
};

const errors = validate(formData, validators);
// errors: {
//   email?: string;
//   password?: string;
//   age?: string;
// }

📊 Utility Types 一覧表

Utility Type 説明 使用例
Partial 全プロパティをオプションに 部分更新
Required 全プロパティを必須に 設定の検証
Readonly 全プロパティを読み取り専用に イミュータブルデータ
Pick<T, K> 特定のプロパティのみ選択 API レスポンス
Omit<T, K> 特定のプロパティを除外 センシティブ情報の除去
Record<K, T> キーと値の型を指定 マッピング
Exclude<T, U> Tから Uを除外 ユニオン型の絞り込み
Extract<T, U> TとUの共通部分 ユニオン型の抽出
NonNullable null/undefined を除外 非null保証
ReturnType 関数の戻り値の型 型推論
Parameters 関数の引数の型 型推論

🎯 ベストプラクティス

1. 型の再利用

// ✅ 良い例: Utility Types で型を再利用
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

type UserDTO = Omit<User, 'password'>;
type UserUpdateRequest = Partial<UserDTO>;

// ❌ 悪い例: 同じ型を再定義
interface UserDTO {
  id: number;
  name: string;
  email: string;
}

2. Template Literal Types で型安全なAPI

// ✅ 良い例
type Endpoint = `/${string}`;
function request(url: Endpoint) { }

request("/api/users");  // ✅ OK
request("api/users");   // ❌ エラー: / で始まっていない

3. infer で柔軟な型推論

// ✅ 良い例: Promise の戻り値を自動推論
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

async function fetchUser() {
  return { id: 1, name: "Alice" };
}

type User = UnwrapPromise<ReturnType<typeof fetchUser>>;
// 手動で型を書く必要がない!

🔍 関連する問題

この記事に関連するクイズ問題:

  • Q13: Omit<T, K> の役割
  • Q17: strictNullChecks の効果
  • Q30: infer キーワードの用途
  • Q34: Template Literal Types の用途
  • Q40: TypeScript の高度な型操作

📝 まとめ

  • Utility Types: 組み込みの型変換ツール(Partial, Omit, Pick など)
  • Mapped Types: プロパティを走査して新しい型を生成
  • Conditional Types: 型レベルの条件分岐(T extends U ? X : Y)
  • infer: Conditional Types 内で型を推論
  • Template Literal Types: 文字列リテラル型の動的生成

次のステップ: 実際のプロジェクトで高度な型システムを活用し、型安全性を高めましょう!

参考リソース: