TypeScriptの基礎 - ユーティリティ型(基本)
概要
ユーティリティ型 (Utility Types) とは、TypeScriptに組み込まれている型変換のための汎用型である。
マップ型 (Mapped Types) を活用して既存の型から新しい型を効率的に生成できる。
ユーティリティ型を使用することで、型定義の重複を避けてコードの保守性を高めることができる。
例えば、全プロパティをオプショナルにしたい場合や、特定のプロパティだけを取り出したい場合に、元の型を修正せずに新しい型を導出できる。
ユーティリティ型はTypeScriptの標準ライブラリに定義されており、追加のインポートなしに使用できる。
内部定義はマップ型やConditional型 (条件型) を組み合わせた構造になっており、TypeScriptの型システムの表現力の高さを示す好例でもある。
| ユーティリティ型 | 説明 |
|---|---|
Partial<T> |
型Tの全プロパティをオプショナル (任意) にする。 |
Required<T> |
型Tの全プロパティを必須にする。 |
Readonly<T> |
型Tの全プロパティを読み取り専用にする。 |
Pick<T, K> |
型Tから指定したプロパティKのみを選択する。 |
Omit<T, K> |
型Tから指定したプロパティKを除外する。 |
Record<K, V> |
キーの型Kと値の型Vを指定したオブジェクト型を生成する。 |
ジェネリクスの基本についてはTypeScriptの基礎 - ジェネリクスの基本のページ、ジェネリクスの制約についてはTypeScriptの基礎 - ジェネリクスの制約のページを参照すること。
Partial<T>
Partial<T> は、型Tの全プロパティをオプショナル (任意) に変換するユーティリティ型である。
オプショナルなプロパティは、値を指定しなかった場合に undefined となる。
内部定義
Partial<T> の内部定義は以下の通りである。
マップ型の構文 [K in keyof T] で型Tの全プロパティを列挙し、? 修飾子を付加することでオプショナルに変換している。
type Partial<T> = {
[K in keyof T]?: T[K];
};
strictNullChecks が有効な場合、オプショナルなプロパティの型は自動的に undefined を含む型 (T[K] | undefined) となる。
実用例 : APIの更新関数
APIの更新エンドポイントでは、全フィールドではなく変更したいプロパティのみを送信することが多い。
Partial<T> を使用することにより、更新時の引数型を簡潔に定義できる。
以下の例では、Partial<User> により changes の全プロパティがオプショナルになるため、更新したいプロパティのみを渡せる。
interface User {
id: string;
name: string;
email: string;
age: number;
}
function updateUser(id: string, changes: Partial<User>): void {
// nameだけを更新、emailとageは指定しなくてもよい
}
updateUser("123", { name: "Alice" }); // OK
実用例 : 設定のマージ
デフォルト設定とユーザ指定の上書き設定をマージする関数にも Partial<T> が有効である。
interface Config {
host: string;
port: number;
timeout: number;
debug: boolean;
}
function mergeConfig(defaults: Config, overrides: Partial<Config>): Config {
return { ...defaults, ...overrides };
}
overrides の型を Partial<Config> にすることにより、上書きしたいプロパティのみを指定できる。
スプレッド演算子 { ...defaults, ...overrides } により、指定されたプロパティはデフォルト値を上書きし、指定されなかったプロパティはデフォルト値が維持される。
Required<T>
Required<T> は、型 T の全プロパティを必須にするユーティリティ型である。
Partial<T> の逆の操作であり、オプショナルなプロパティの ? 修飾子を全て除去する。
内部定義
Required<T> の内部定義は以下の通りである。
-? という修飾子により、マップ型で列挙した全プロパティからオプショナル修飾子 ? を除去している。
type Required<T> = {
[K in keyof T]-?: T[K];
};
-? はマップ型専用の修飾子除去構文であり、通常のTypeScript記法には存在しない特殊な構文である。
実用例 : バリデーション後の型
フォームの入力値は全フィールドがオプショナルで定義されることが多いが、バリデーション後は全フィールドが存在することが保証される。
このような場面で Required<T> を使用することにより、バリデーション後の型を安全に表現できる。
interface UserInput {
name?: string;
email?: string;
age?: number;
}
function validateUser(input: UserInput): Required<UserInput> {
if (!input.name || !input.email || !input.age) {
throw new Error("All fields are required");
}
return input as Required<UserInput>;
}
バリデーション後の戻り値型を Required<UserInput> とすることにより、呼び出し元では全プロパティが存在することが型システムによって保証される。
Readonly<T>
Readonly<T> は、型 T の全プロパティを読み取り専用 (readonly) にするユーティリティ型である。
読み取り専用プロパティへの再代入はコンパイルエラーとなる。
内部定義
Readonly<T> の内部定義は以下の通りである。
マップ型の各プロパティに readonly 修飾子を付加することで読み取り専用に変換している。
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
Object.freeze()との関連
Readonly<T> は型レベルの制約であり、実行時には効果を持たない。
コンパイル後のJavaScriptには readonly 修飾子の情報は残らないため、実行時にプロパティを変更することは技術的には可能である。
実行時のイミュータビリティを確保するためには、Object.freeze() と組み合わせて使用する。
Readonly<T>- コンパイル時にプロパティへの再代入をエラーとして検出する。
Object.freeze()- 実行時にオブジェクトのプロパティ変更を防ぐ。
実用例 : イミュータブルな状態管理
設定オブジェクトや定数として扱うオブジェクトに Readonly<T> を適用することで、誤った変更を防げる。
interface Config {
host: string;
port: number;
timeout: number;
debug: boolean;
}
const config: Readonly<Config> = {
host: "localhost",
port: 8080,
timeout: 5000,
debug: false
};
config.host = "127.0.0.1"; // コンパイルエラー : 読み取り専用プロパティに代入できない
Readonly<T> はトップレベルのプロパティのみを読み取り専用にする。
ネストされたオブジェクトのプロパティは読み取り専用にならないため、深いイミュータビリティが必要な場合は Readonly<T> を再帰的に適用するか、ライブラリを使用することを検討する。
Pick<T, K>
Pick<T, K> は、型Tから指定したプロパティKのみを選択して新しい型を生成するユーティリティ型である。
K には型 T のプロパティ名をユニオン型で指定する。
内部定義
Pick<T, K> の内部定義は以下の通りである。
型パラメータKに extends keyof T の制約を設けることにより、存在しないプロパティ名の指定をコンパイル時に検出できる。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
実用例 : APIレスポンスの一部取得
APIレスポンスの型から、クライアントに公開してよいプロパティのみを選択した型を定義できる。
interface UserProfile {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
type PublicUserInfo = Pick<UserProfile, "id" | "name" | "email">;
// { id: string; name: string; email: string; }
password 等の機密情報を除いた型を Pick<T, K> で定義することで、APIレスポンスの型安全性を高められる。
実用例 : フォームの型定義
フォームの種類ごとに必要なフィールドが異なる場合、ベースとなる型から必要なプロパティを選択することで型の重複を避けられる。
interface UserForm {
username: string;
password: string;
email: string;
displayName: string;
}
type LoginForm = Pick<UserForm, "username" | "password">;
type RegisterForm = Pick<UserForm, "username" | "password" | "email">;
フォームの型を個別に定義する代わりに Pick<T, K> を使用することで、元の型への変更が全てのフォーム型に自動的に反映される。
Omit<T, K>
Omit<T, K> は、型Tから指定したプロパティKを除外して新しい型を生成するユーティリティ型である。
Pick<T, K> の逆の操作であり、除外するプロパティを指定する。
内部定義
Omit<T, K> の内部定義は以下の通りである。
Omit<T, K> は Pick と Exclude を組み合わせて実装されている。
Exclude<keyof T, K> でTの全プロパティキーからKを除いたキーを取得し、そのキーのみで Pick することで除外を実現している。
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
実用例: IDを除いた入力型
データの新規作成時は、サーバ側で自動生成される id や createdAt 等を除いた型を入力として受け取ることが多い。
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; }
Omit<T, K> を使用することにより、元の型に新しいプロパティが追加された場合も自動的に入力型に反映される。
実用例 : コンポーネントの型拡張
既存の型を拡張する時に、一部のプロパティを置き換えたい場合に Omit<T, K> が有用である。
以下の例では、BaseButtonProps から disabled プロパティを除いた上で size プロパティを追加した PrimaryButtonProps を定義している。
interface BaseButtonProps {
label: string;
onClick: () => void;
disabled: boolean;
}
interface PrimaryButtonProps extends Omit<BaseButtonProps, "disabled"> {
size: "small" | "medium" | "large";
}
Record<K, V>
Record<K, V> は、キーの型 K と値の型 V を指定したオブジェクト型を生成するユーティリティ型である。
Kにはユニオン型やリテラル型等、オブジェクトのキーとして使用できる型を指定する。
内部定義
Record<K, V> の内部定義は以下の通りである。
型パラメータKに extends keyof any の制約を設けることで、
Kが string、number、symbol のいずれか、またはそのユニオン型であることを保証している。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
実用例: ルックアップテーブル
特定のキー集合に対してそれぞれの値を紐付けるルックアップテーブルを型安全に定義できる。
type Status = "pending" | "completed" | "failed";
const statusMessages: Record<Status, string> = {
pending: "処理中です",
completed: "完了しました",
failed: "失敗しました"
};
Status 型のユニオン型をキーに指定することで、全ての状態値に対してメッセージが定義されていることをコンパイル時に検証できる。
Status 型に新しい値が追加された場合、statusMessages にも対応するエントリを追加しなければコンパイルエラーとなる。
実用例 : 状態マップ
IDをキーとしてエンティティを管理する状態マップにも Record<K, V> が有用である。
interface UserState {
name: string;
email: string;
isActive: boolean;
}
type UserMap = Record<string, UserState>;
const users: UserMap = {
"user-001": { name: "Alice", email: "alice@example.com", isActive: true },
"user-002": { name: "Bob", email: "bob@example.com", isActive: false }
};
キーの型を string にすることで任意の文字列IDを使用でき、値の型を UserState にすることでオブジェクトの構造が型安全に保証される。
ユーティリティ型の組み合わせ
複数のユーティリティ型を組み合わせることで、より細かい型変換が実現できる。
ただし、組み合わせの順序によって結果が異なる場合があるため注意が必要である。
Partial + Pick
特定のプロパティを選択した上で全てをオプショナルにする場合は、Partial<Pick<T, K>> を使用する。
interface User {
id: string;
name: string;
email: string;
age: number;
}
type UpdateUserInput = Partial<Pick<User, "name" | "email">>;
// { name?: string; email?: string; }
Pick<User, "name" | "email"> でnameとemailのみの型を生成し、Partial<...> でそれらをオプショナルにしている。
Omit + Partial
特定のプロパティを除外した上で残りを全てオプショナルにする場合は、Partial<Omit<T, K>> を使用する。
interface Product {
id: string;
name: string;
price: number;
description: string;
createdAt: Date;
}
type PartialProductUpdate = Partial<Omit<Product, "id" | "createdAt">>;
// { name?: string; price?: number; description?: string; }
Required + Pick
特定のプロパティを選択した上で全てを必須にする場合は、Required<Pick<T, K>> を使用する。
interface FormData {
username?: string;
password?: string;
email?: string;
displayName?: string;
}
type RequiredFormData = Required<Pick<FormData, "username" | "password">>;
// { username: string; password: string; }
APIのCRUD型定義パターン
APIのCRUD操作で必要な型を、1つのベース型から複数のユーティリティ型の組み合わせで導出できる。
interface ApiUser {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
updatedAt: Date;
}
// 作成リクエスト : idとタイムスタンプはサーバ側で生成するため除外する
type CreateUserRequest = Omit<ApiUser, "id" | "createdAt" | "updatedAt">;
// 更新リクエスト : id、パスワード、タイムスタンプを除いた残りをオプショナルにする
type UpdateUserRequest = Partial<Omit<ApiUser, "id" | "password" | "createdAt" | "updatedAt">>;
// 公開レスポンス : 機密情報であるpasswordを除いた公開可能なプロパティのみを返す
type PublicUserResponse = Pick<ApiUser, "id" | "name" | "email">;
上記のパターンにより、ApiUser を唯一の型定義ソースとして管理でき、プロパティの追加や変更が全ての派生型に自動的に反映される。
組み合わせに関して、以下の点に注意すること。
Partial<Pick<T, K>>とPick<Partial<T>, K>は同じ結果になるが、
意図を明確にするために外側から内側へ読める順序で記述することを推奨する。- 組み合わせが複雑になりすぎた場合は、中間的な型に名前を付けて可読性を維持する。
関連情報