TypeScript のユーティリティ型: Partial, Pick, Omit, Record
1. 概要
TypeScript コードを書いていると、既存の型に似ているけれど、いくつかのフィールドが不足または過剰な型が必要な状況に直面するでしょう。新しい interface を宣言する代わりに、TypeScript は既存の型を変換するためのユーティリティ型のグループを提供しています。この記事では、最も頻繁に使用される 4 つのユーティリティ型を紹介します:Partial、Pick、Omit、Record です。実例とよくあるエラーを含めて説明します。この記事を読み終わると、interface を手動で書き直す代わりに、各状況に適切なユーティリティ型を選べるようになります。
2. Partial<T>
Partial<T> は T のすべてのフィールドをオプショナルに変換します。呼び出し元が一部のフィールドだけを渡す必要がある update 関数を書く際に便利です。
interface Customer {
id: number;
name: string;
email: string;
phone: string;
}
function updateCustomer(id: number, changes: Partial<Customer>): Customer {
const current = findCustomerById(id); // この関数はすでに存在すると仮定
return { ...current, ...changes };
}
updateCustomer(1, { email: "new@mail.com" });
Partial<Customer> は次と同等の型を作成します:
interface PartialCustomer {
id?: number;
name?: string;
email?: string;
phone?: string;
}
おかげで、updateCustomer は email だけを持つ object を受け入れ、フィールド不足エラーは発生しません。
注意:
Partialは第 1 階層のみをオプショナルにします(shallow)。Customerにネストされたオブジェクトのフィールドがある場合、内側のフィールドは依然として required のままです。深い partial が必要な場合は、自分で再帰的な型を書くか、type-festのようなライブラリを使用する必要があります。
3. Pick<T, K>
Pick<T, K> は、キーのリスト K に従って T からフィールドのサブセットを選択します。大きな型の一部のデータだけが必要な場合、例えば client に返す DTO を作成する際に使用します。
interface Order {
id: string;
customerId: number;
totalAmount: number;
items: string[];
createdAt: Date;
internalNote: string;
}
type OrderSummary = Pick<Order, "id" | "totalAmount" | "createdAt">;
const summary: OrderSummary = {
id: "ORD-001",
totalAmount: 250_000,
createdAt: new Date(),
};
OrderSummary は 3 つのフィールドだけを持ちます。items や internalNote フィールドを summary に追加しようとすると、compiler は Object literal may only specify known properties エラーを報告します。
3.1. Pick を union キーと組み合わせる
Pick に渡すキーは union 文字列リテラルなので、再利用するために変数に分離できます:
type OrderPublicFields = "id" | "totalAmount" | "createdAt";
type OrderSummary = Pick<Order, OrderPublicFields>;
type OrderListItem = Pick<Order, OrderPublicFields | "customerId">;
この方法により、同じ entity から複数の DTO バリアントがある場合、複数の場所でキーのリストを繰り返すことを避けられます。
4. Omit<T, K>
Omit<T, K> は Pick の逆を行います:指定されたキーを除外し、残りを保持します。通常、機密フィールドまたは内部フィールドを非表示にしたい場合に使用します。
interface User {
id: number;
username: string;
passwordHash: string;
role: string;
}
type PublicUser = Omit<User, "passwordHash">;
function toPublicUser(user: User): PublicUser {
const { passwordHash, ...rest } = user;
return rest;
}
const { passwordHash, ...rest } = user という行は passwordHash を分離し、rest には id、username、role だけが残ります — これは PublicUser 型と正確に一致します。
// console.log(toPublicUser(user)) の出力
{ id: 1, username: "nonla_dev", role: "ADMIN" }
注意:
OmitはPickとExcludeに基づいて定義され、object リテラル型ではよく機能しますが、複雑なケースではオーバーロードや discriminated union を正しく削除できない場合があります。union 型(type A = B | C)の場合、Omit<A, K>は omit する前にメンバーをマージしてしまい、discriminant を失う可能性があります。複雑な union に直面した場合は、Omitを直接使用するのではなく、型を手動で書くか、条件付き mapped type テクニックを検討してください。
5. Record<K, T>
Record<K, T> は、すべてのキーが K に属し、各キーが T 型の値を持つ object 型を作成します。キーが固定のマップ/辞書をモデル化する際に便利です。
type PaymentStatus = "PENDING" | "PAID" | "CANCELLED";
interface StatusInfo {
label: string;
color: string;
}
const statusDisplay: Record<PaymentStatus, StatusInfo> = {
PENDING: { label: "支払い待機中", color: "orange" },
PAID: { label: "支払い済み", color: "green" },
CANCELLED: { label: "キャンセル済み", color: "gray" },
};
function getStatusLabel(status: PaymentStatus): string {
return statusDisplay[status].label;
}
statusDisplay が Record<PaymentStatus, StatusInfo> として宣言されているため、compiler は 3 つすべてのキー PENDING、PAID、CANCELLED を持つことを強制します。キーの 1 つを忘れた場合、TypeScript はコンパイル時にエラーを報告します — これは通常の { [key: string]: StatusInfo } index signature を使用するのと比べて強力です(index signature はキーの完全性をチェックしません)。
5.1. 動的キーの Record
キーが union リテラルではなく、一般的な string の場合、Record<string, T> は index signature と同等です:
const cache: Record<string, number> = {};
cache["order-1"] = 100;
このケースでは「完全なキー」チェックが行われません。string には無数の値があるためです。Record<string, T> は自由な辞書が本当に必要な場合にのみ使用してください。そうでない場合は、exhaustive check を活用するため union リテラルを優先すべきです。
6. 複数のユーティリティ型を組み合わせる
ユーティリティ型はお互いにネストでき、新しい interface を書くことなく、より複雑な型を作成できます。
interface Product {
id: string;
name: string;
price: number;
stock: number;
supplierNote: string;
}
type ProductUpdateRequest = Partial<Omit<Product, "id">>;
function updateProduct(id: string, data: ProductUpdateRequest) {
// repository.update(id, data) を呼び出す
}
updateProduct("P001", { price: 199_000 });
Omit<Product, "id"> は id フィールドを除外し(主キーの編集は不可)、その後 Partial は残りのフィールドをオプショナルにします。結果:呼び出し元は変更したいフィールドだけを渡す必要があり、id は渡すことができません。
7. 動作を確認するテストを書く
ユーティリティ型はコンパイル時にのみ存在するため、実際に確認する方法は、それらの型を使用する関数の unit test を書き、runtime の動作が期待どおりであることを確認することです。例として Vitest を使用します:
<!-- package.json 依存関係の同等物(Maven ではなく) -->
import { describe, it, expect } from "vitest";
interface User {
id: number;
username: string;
passwordHash: string;
role: string;
}
type PublicUser = Omit<User, "passwordHash">;
function toPublicUser(user: User): PublicUser {
const { passwordHash, ...rest } = user;
return rest;
}
describe("toPublicUser", () => {
it("passwordHash を結果から削除する", () => {
const user: User = {
id: 1,
username: "nonla_dev",
passwordHash: "hash-123",
role: "ADMIN",
};
const result = toPublicUser(user);
expect(result).toEqual({
id: 1,
username: "nonla_dev",
role: "ADMIN",
});
expect(result).not.toHaveProperty("passwordHash");
});
});
このテストは、toPublicUser 関数が型において正しいだけでなく、runtime でも正しいことを確認します — passwordHash は実際に返された object から消えています。型レベルで「隠されている」だけではありません。
8. よくあるエラーと使用すべきでない場合
Pick/Omitは実データを削除することだと勘違いする: どちらも型のみを変更し、object の runtime に影響しません。destructuring(const { x, ...rest } = obj)が実際にフィールドをデータから削除します。- API から返される object に
Partialを乱用する: response が実際には常にすべてのフィールドが存在する場合、Partialを使用すると、呼び出しコードがあちこちで不要なundefinedチェックを強制されます。Partialは input(update/patch)に対してのみ使用し、出力が確実に完全な場合には使用しないでください。 - 複雑な union 型を使用した
Omit: セクション 4 で述べたように、Omitは discriminated union を失う可能性があります。型が複数のバリアント(例:Shape = Circle | Square)の union である場合は、Omitを直接使用するのではなく、型を手動で書くか、条件付き mapped type テクニックを使用してください。 - interface の代わりに
Record<string, T>を使用する: すべてのキーが事前にわかっている場合は、union リテラルを宣言し、Record<Key, T>を使用して exhaustive check を実現してください。気軽にRecord<string, T>を使用するとコンパイル時にキー不足/過剰を検出する能力を失います。
注意: これらのユーティリティ型はすべてコンパイル時にのみ機能します。JavaScript にビルドされた後、それらは完全に消えます — runtime のオーバーヘッドはありませんが、runtime の検証もありません。データが外部から来た場合(API、JSON ファイル)、Zod や io-ts などのライブラリを使用して検証する必要があります。
9. 結論
Partial、Pick、Omit、Record は、重複した interface を書き直す代わりに型を再利用するのに役立ちます。Partial は update 型の input に適しており、Pick/Omit は大きな entity から DTO を作成するために使用され、Record は固定キーの辞書に最適であり、compiler の exhaustive check を活用します。各ユーティリティ型の制限を理解する — 特に Partial の shallow な動作と Omit の union 型の問題 — 後で難しくデバッグしにくい型エラーを避けるのに役立ちます。