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: 文字列リテラル型の動的生成
次のステップ: 実際のプロジェクトで高度な型システムを活用し、型安全性を高めましょう!
参考リソース: