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 型の問題 — 後で難しくデバッグしにくい型エラーを避けるのに役立ちます。

typescriptutility-typestype-systemfrontend