TypeScriptのユーティリティ型入門:Partial・Pick・Omit・Record・Readonlyの使い分け
会員データの一覧表示や更新処理を題材に、ユーティリティ型とkeyofの使い分けを解説。型を省略しても実データは消えない点や、任意項目とundefinedの違いも確認します。
会員情報の型を作った後、一覧表示用、登録用、更新用にも似た型が必要になることがあります。項目を何度も書くと、仕様変更のたびに修正箇所が増えます。ユーティリティ型を使うと、元の型から用途に合う型を作れます。ここでは、オブジェクト型とジェネリクスの基礎を学んだ方向けに、会員管理の小さな例で使い分けを整理します。
【1. どの型を使えばよいか】
・Pick<T, K>:一覧に表示するIDと名前など、使う項目を選ぶ。
・Omit<T, K>:登録時にはまだ存在しないIDなど、特定の項目を除いた型を作る。
・Partial<T>:更新画面から届く「変更した項目だけ」のように、項目を任意にする。
・Record<K, V>:役割名から表示名への対応など、キーと値の型を決める。
・Readonly<T>:その型を通じたプロパティへの再代入を禁止する。
これらはTypeScriptに組み込まれた型です。定義と適用例は 公式のUtility Types(https://www.typescriptlang.org/docs/handbook/utility-types.html) にまとまっています。
【2. 会員データから用途別の型を作る】
次の例では、IDのない登録データ、名前とメールだけを変更できる更新データ、IDと名前だけの一覧表示を作ります。Partial<Member> だけにするとIDや権限まで変更対象になるため、更新を許す項目を Pick で絞ってから任意にしています。
ファイル: members.ts
type Member = {
id: number;
name: string;
email: string;
role: "reader" | "editor";
};
type MemberSummary = Pick<Member, "id" | "name">;
type NewMember = Omit<Member, "id">;
type MemberPatch = Partial<Pick<Member, "name" | "email">>;
const roleLabels: Record<Member["role"], string> = {
reader: "閲覧者",
editor: "編集者",
};
function updateMember(member: Member, patch: MemberPatch): Member {
return { ...member, ...patch };
}
function getField<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
function toSummary(member: Member): MemberSummary {
// 表示する項目だけを持つ、新しいオブジェクトを作る。
return { id: member.id, name: member.name };
}
const input: NewMember = {
name: "あかり",
email: "akari@example.com",
role: "reader",
};
const member: Member = { id: 7, ...input };
const updated = updateMember(member, { name: "あかりさん" });
const snapshot: Readonly<Member> = updated;
console.log(getField(updated, "name")); // あかりさん
console.log(roleLabels[updated.role]); // 閲覧者
console.log(JSON.stringify(toSummary(snapshot))); // {"id":7,"name":"あかりさん"}
console.log(member.name); // あかり(元の値はそのまま)
この更新関数は新しいオブジェクトを返すため、元の会員名は「あかり」のままです。Member["role"] はroleプロパティの型、つまり "reader" | "editor" を取り出しています。この組み合わせの Record では、どちらかの表示名を書き忘れると型エラーになります。
【3. keyofはオブジェクトのキーを型として取り出す】
keyof Member は "id" | "name" | "email" | "role" に相当します。例の K extends keyof T は、渡すキーを対象のオブジェクトが持つキーに制限します。getField(updated, "name") の結果はstring型になり、"missing" を指定すると型エラーになります。仕組みは 公式のkeyof解説(https://www.typescriptlang.org/docs/handbook/2/keyof-types.html) も参照してください。
【4. 型を減らしても、実際のプロパティは消えない】
const summary: MemberSummary = updated; と代入しても、実際のオブジェクトにはemailやroleが残っています。型は実行時のデータを加工しないためです。公開APIから返す項目を減らしたい場合は、例の toSummary() のように必要な項目だけを取り出します。秘密情報を除く処理をOmitやPickだけで済ませないことが大切です。
【5. PartialとReadonlyは、ネストの内側まで変えない】
例えば { profile: { name: string } } にPartialを適用するとprofile自体を省略できますが、profileを指定する場合にはnameが必要です。同様にReadonlyを付けても、入れ子のオブジェクトまで自動で読み取り専用にはなりません。また、Readonlyは Object.freeze() のような実行時の凍結処理ではありません。オブジェクトの参照と変更については 公式のObject Types(https://www.typescriptlang.org/docs/handbook/2/objects.html) を確認してください。
【6. 更新データのundefinedをどう扱うか】
この記事の例は exactOptionalPropertyTypes: true で確認しています。この設定では name?: string に対して、nameを省略することはできても、明示的に name: undefined を渡すことはできません。「変更しない」と「値を消す」を混同しにくくなります。設定の違いは 公式ドキュメント(https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html) を参照してください。
フォームやAPIから届く更新データには、実行時の検証も必要です。型定義だけでは、受け取った値が空文字でないこと、メールの形式、操作権限などは保証されません。上の関数は、検証済みのデータを扱う学習用の例です。
【実行方法と練習問題】
作業用の新しいフォルダに members.ts と次の設定を保存してください。TypeScriptをインストールした環境で npx tsc -p .、続いて node dist/members.js を実行できます。
ファイル: tsconfig.json
{"compilerOptions":{"target":"ES2022","module":"NodeNext","strict":true,"exactOptionalPropertyTypes":true,"noUncheckedIndexedAccess":true,"outDir":"dist","types":[],"lib":["ES2022","DOM"]},"include":["*.ts"]}
練習としてroleに "admin" を追加し、roleLabelsの書き漏れを確認しましょう。次に、更新でroleを変更できるようにする場合、どの型を変更するのか考えてください。型を変更できても、管理者かどうかを判定する処理は別に必要です。
【よくある質問】
【Partialを使うと空のオブジェクトも渡せますか?】
渡せます。少なくとも1項目の変更が必要なら、空の更新を拒否する検証を追加します。すべての項目を任意にすることと、業務ルールを満たすことは別の条件です。
【すべての型をユーティリティ型で作るべきですか?】
必須ではありません。元の型の変更に追従させたい場合に役立ちます。一方、APIの入出力が独立した契約なら、その用途の型を明示的に定義する方が変更の影響を読み取りやすい場合もあります。
【参考資料】
・TypeScript: Utility Types(https://www.typescriptlang.org/docs/handbook/utility-types.html)
・TypeScript: keyof Type Operator(https://www.typescriptlang.org/docs/handbook/2/keyof-types.html)
・TypeScript: Object Types(https://www.typescriptlang.org/docs/handbook/2/objects.html)
・TSConfig: exactOptionalPropertyTypes(https://www.typescriptlang.org/tsconfig/exactOptionalPropertyTypes.html)