🎨

TypeScript Utility Types【文字列・Union型編】

に公開

はじめに

この記事では、TypeScript の文字列操作や Union 型の操作に特化した Utility Types を実例を交えて詳しく解説していきます。

文字列操作の Utility Types

Uppercase<T> - 文字列を大文字に変換

Uppercase<T>は、文字列リテラル型を大文字に変換します。

type Greeting = "hello world";
type UpperGreeting = Uppercase<Greeting>; // "HELLO WORLD"

// 使用例:定数の生成
type HttpMethod = "get" | "post" | "put" | "delete";
type HttpMethodUpper = Uppercase<HttpMethod>; // "GET" | "POST" | "PUT" | "DELETE"

// 実践例:環境変数の型定義
type Environment = "development" | "staging" | "production";
type EnvironmentVar = `NODE_ENV_${Uppercase<Environment>}`;
// "NODE_ENV_DEVELOPMENT" | "NODE_ENV_STAGING" | "NODE_ENV_PRODUCTION"

const config = {
  NODE_ENV_DEVELOPMENT: "dev-config",
  NODE_ENV_STAGING: "stage-config",
  NODE_ENV_PRODUCTION: "prod-config",
} satisfies Record<EnvironmentVar, string>;

Lowercase<T> - 文字列を小文字に変換

Lowercase<T>は、文字列リテラル型を小文字に変換します。

type ApiEndpoint = "GET /api/users" | "POST /api/users";
type NormalizedEndpoint = Lowercase<ApiEndpoint>;
// "get /api/users" | "post /api/users"

// 使用例:CSS プロパティの正規化
type CSSProperty = "backgroundColor" | "fontSize" | "marginTop";
type CSSPropertyNormalized = Lowercase<CSSProperty>;
// "backgroundcolor" | "fontsize" | "margintop"

// より実践的な例:API エンドポイントの管理
type ApiMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiPath = "/users" | "/posts" | "/comments";

type ApiEndpointKey = `${Lowercase<ApiMethod>}${ApiPath}`;
// "get/users" | "get/posts" | "get/comments" | "post/users" | ...

type ApiEndpoints = Record<ApiEndpointKey, () => Promise<any>>;

Capitalize<T> - 最初の文字を大文字に変換

Capitalize<T>は、文字列リテラル型の最初の文字のみを大文字に変換します。

type Action = "createUser" | "deleteUser" | "updateUser";
type ActionLabel = Capitalize<Action>;
// "CreateUser" | "DeleteUser" | "UpdateUser"

// 使用例:React コンポーネント名の生成
type ComponentType = "button" | "input" | "select" | "textarea";
type ComponentName = `${Capitalize<ComponentType>}Component`;
// "ButtonComponent" | "InputComponent" | "SelectComponent" | "TextareaComponent"

// 実践例:イベント名の統一
type EventType = "click" | "hover" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventType>}`;
// "onClick" | "onHover" | "onFocus" | "onBlur"

interface EventHandlers extends Record<EventHandler, (event: Event) => void> {}

Uncapitalize<T> - 最初の文字を小文字に変換

Uncapitalize<T>は、文字列リテラル型の最初の文字のみを小文字に変換します。

type ClassName = "Button" | "Input" | "Modal";
type InstanceName = Uncapitalize<ClassName>;
// "button" | "input" | "modal"

// 使用例:プロパティ名の生成
type ServiceClass = "UserService" | "PostService" | "AuthService";
type ServiceProperty = Uncapitalize<ServiceClass>;
// "userService" | "postService" | "authService"

interface ServiceContainer extends Record<ServiceProperty, any> {}

Union 型操作の Utility Types

Exclude<T, U> - Union 型から特定の型を除外

Exclude<T, U>は、型Tから型Uに割り当て可能な型を除外します。

type AllColors = "red" | "blue" | "green" | "yellow" | "purple";
type WarmColors = "red" | "yellow";
type CoolColors = Exclude<AllColors, WarmColors>; // "blue" | "green" | "purple"

// 使用例:権限の管理
type AllPermissions = "read" | "write" | "delete" | "admin";
type GuestPermissions = Exclude<AllPermissions, "delete" | "admin">; // "read" | "write"

// 実践例:APIレスポンスからエラー型を除外
type ApiResponse<T> = T | { error: string };
type SuccessResponse<T> = Exclude<ApiResponse<T>, { error: string }>; // T のみ

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

type UserResponse = ApiResponse<User>;
type UserSuccess = SuccessResponse<User>; // User型のみ

// より高度な例:関数の引数からオプション型を除外
type RequiredKeys<T> = {
  [K in keyof T]: T extends Record<K, T[K]> ? K : never;
}[keyof T];

type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>;

Extract<T, U> - Union 型から特定の型のみを抽出

Extract<T, U>は、型Tから型Uに割り当て可能な型のみを抽出します。

type AllTypes = string | number | boolean | object;
type PrimitiveTypes = Extract<AllTypes, string | number | boolean>; // string | number | boolean

// 使用例:特定の条件を満たす型の抽出
type ApiEndpoint =
  | "GET /users"
  | "POST /users"
  | "GET /posts"
  | "DELETE /posts";
type GetEndpoints = Extract<ApiEndpoint, `GET ${string}`>; // "GET /users" | "GET /posts"

// 実践例:関数型のみを抽出
interface MixedObject {
  name: string;
  age: number;
  getName: () => string;
  setAge: (age: number) => void;
  isActive: boolean;
}

type FunctionKeys = {
  [K in keyof MixedObject]: MixedObject[K] extends (...args: any[]) => any
    ? K
    : never;
}[keyof MixedObject];

type FunctionProperties = Pick<MixedObject, FunctionKeys>;
// { getName: () => string; setAge: (age: number) => void; }

// より高度な例:条件付き型抽出
type ExtractByType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

type StringKeys = ExtractByType<MixedObject, string>; // "name"
type NumberKeys = ExtractByType<MixedObject, number>; // "age"

NonNullable<T> - null/undefined を除外

NonNullable<T>は、型Tから null と undefined を除外します。

type NullableString = string | null | undefined;
type ValidString = NonNullable<NullableString>; // string

// 使用例:配列の型安全なフィルタリング
const mixedArray: (string | null | undefined)[] = [
  "hello",
  null,
  "world",
  undefined,
];
const validStrings: NonNullable<(typeof mixedArray)[0]>[] = mixedArray.filter(
  (item): item is NonNullable<typeof item> => item != null
);

// 実践例:API レスポンスの型安全な処理
interface ApiUser {
  id: number;
  name: string | null;
  email: string | null;
  avatar?: string;
}

type RequiredUserInfo = {
  [K in keyof ApiUser]: NonNullable<ApiUser[K]>;
};
// { id: number; name: string; email: string; avatar: string; }

// より高度な例:Deep NonNullable
type DeepNonNullable<T> = {
  [K in keyof T]: T[K] extends object
    ? DeepNonNullable<NonNullable<T[K]>>
    : NonNullable<T[K]>;
};

interface NestedData {
  user: {
    profile: {
      name: string | null;
      avatar?: string;
    } | null;
  } | null;
}

type SafeNestedData = DeepNonNullable<NestedData>;
// user.profile.name と user.profile.avatar が必須になる

その他の Utility Types

Readonly<T> - 全プロパティを読み取り専用に

Readonly<T>は、型Tのすべてのプロパティを読み取り専用にします。

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

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

const user: ReadonlyUser = { id: 1, name: "Alice", email: "alice@example.com" };
// user.name = "Bob"; // ❌ エラー: 読み取り専用プロパティ

// 実践例:設定オブジェクトの保護
interface AppConfig {
  apiUrl: string;
  timeout: number;
  retries: number;
}

const config: Readonly<AppConfig> = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
};

// Deep Readonly の実装
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface NestedConfig {
  api: {
    url: string;
    timeout: number;
  };
  cache: {
    ttl: number;
    maxSize: number;
  };
}

type SafeConfig = DeepReadonly<NestedConfig>;
// すべてのネストされたプロパティが readonly になる

実践的な使用例

型安全な API 設定管理

// 環境設定の型定義
type Environment = "development" | "staging" | "production";
type LogLevel = "debug" | "info" | "warn" | "error";

// 設定の基本構造
interface BaseConfig {
  apiUrl: string;
  timeout: number;
  logLevel: LogLevel;
}

// 環境別設定
type EnvironmentConfig = Record<Environment, BaseConfig>;

// 環境変数キー生成
type EnvVarKey<T extends string> = `APP_${Uppercase<T>}`;
type ConfigKeys = EnvVarKey<Environment>; // "APP_DEVELOPMENT" | "APP_STAGING" | "APP_PRODUCTION"

// 型安全な設定読み込み
function loadConfig(env: Environment): Readonly<BaseConfig> {
  const configs: EnvironmentConfig = {
    development: {
      apiUrl: "http://localhost:3000",
      timeout: 10000,
      logLevel: "debug",
    },
    staging: {
      apiUrl: "https://staging-api.example.com",
      timeout: 8000,
      logLevel: "info",
    },
    production: {
      apiUrl: "https://api.example.com",
      timeout: 5000,
      logLevel: "warn",
    },
  };

  return configs[env];
}

// 使用例
const devConfig = loadConfig("development");
// devConfig.apiUrl = "other"; // ❌ エラー: 読み取り専用

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

// イベント名の型定義
type DomEvent = "click" | "hover" | "focus" | "blur";
type CustomEvent = "userLogin" | "userLogout" | "dataUpdated";
type AllEvents = DomEvent | CustomEvent;

// イベントハンドラーの型生成
type EventHandlerName<T extends string> = `on${Capitalize<T>}`;
type DomEventHandlers = EventHandlerName<DomEvent>;
// "onClick" | "onHover" | "onFocus" | "onBlur"

// イベントデータの型定義
interface EventDataMap {
  click: { x: number; y: number; target: Element };
  hover: { element: Element };
  focus: { element: Element };
  blur: { element: Element };
  userLogin: { userId: string; timestamp: number };
  userLogout: { userId: string; timestamp: number };
  dataUpdated: { type: string; data: any };
}

// 型安全なイベントエミッター
class TypeSafeEventEmitter {
  private listeners: Partial<Record<AllEvents, Function[]>> = {};

  on<T extends AllEvents>(
    event: T,
    handler: (data: EventDataMap[T]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(handler);
  }

  emit<T extends AllEvents>(event: T, data: EventDataMap[T]): void {
    const handlers = this.listeners[event];
    if (handlers) {
      handlers.forEach((handler) => handler(data));
    }
  }

  // 特定のタイプのイベントのみ取得
  getDomEventTypes(): Extract<AllEvents, DomEvent> {
    // 実装は省略
    return "click"; // 例
  }

  getCustomEventTypes(): Exclude<AllEvents, DomEvent> {
    // 実装は省略
    return "userLogin"; // 例
  }
}

// 使用例
const emitter = new TypeSafeEventEmitter();

emitter.on("click", ({ x, y, target }) => {
  console.log(`Clicked at (${x}, ${y}) on`, target);
});

emitter.on("userLogin", ({ userId, timestamp }) => {
  console.log(`User ${userId} logged in at ${timestamp}`);
});

emitter.emit("click", { x: 100, y: 200, target: document.body }); // ✅ 型安全
// emitter.emit("click", { x: 100 }); // ❌ エラー: y と target が不足

まとめ

この文字列・Union 型編では、文字列操作と Union 型操作に特化した 7 つの Utility Types を紹介しました:

文字列操作

  • Uppercase<T> - 文字列を大文字に変換
  • Lowercase<T> - 文字列を小文字に変換
  • Capitalize<T> - 最初の文字を大文字に変換
  • Uncapitalize<T> - 最初の文字を小文字に変換

Union 型操作

  • Exclude<T, U> - Union 型から特定の型を除外
  • Extract<T, U> - Union 型から特定の型のみを抽出
  • NonNullable<T> - null/undefined を除去

その他

  • Readonly<T> - プロパティを読み取り専用に

これらを適切に組み合わせることで、型安全で保守性の高いコードを書くことができます。

参考文献

https://www.typescriptlang.org/docs/handbook/utility-types.html
https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

Discussion