🎨
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>
- プロパティを読み取り専用に
これらを適切に組み合わせることで、型安全で保守性の高いコードを書くことができます。
参考文献
Discussion