Partial<T> は、T のすべてのプロパティを省略可能にする組み込みのユーティリティ型です。更新やパッチにはこれが自然な型です。呼び出し側は変わったフィールドだけを送ります。
Partial<User> は { id?: number; name?: string; email?: string } です。渡したフィールドはコンパイラーがきちんとチェックします。{ nmae: "x" } や { name: 42 } はエラーで、それが Partial をゆるい object や any の引数より優れたものにしています。
Partial の定義
Partial はTypeScriptの標準ライブラリにある1行のマップ型です:
type Partial<T> = {
[P in keyof T]?: T[P];
};
T の各キー P について、同じ型の省略可能なプロパティを宣言します。keyof T に対してマップするので、readonly を持っていたプロパティはそれを保ちます。普通の型なので、実行時の効果はありません。changes として渡すオブジェクトは、どちらにしても同じオブジェクトです。
Partial から読むと T | undefined になる
Partial<T> のすべてのプロパティは欠けている可能性があるので、読むとプロパティの型に undefined が加わります。コンパイラーは欠けている場合の処理を求めます:
{ ...defaults, ...opts } は、Partial<Options> を完全な Options に戻す一般的な方法です。デフォルト値を先にスプレッドし、与えられた値でそれを上書きします。
明示的な undefined の落とし穴
省略可能なプロパティは欠けていてもよいですが、undefined という値で存在していてもかまいません。オブジェクトのスプレッドはその undefined を本当の値の上にコピーし、結果の型にはそれが表れません:
これは、空のフィールドが undefined になるフォームやクエリ文字列からパッチを作るときに問題になります。コンパイラーのオプション exactOptionalPropertyTypes(strict には含まれません)を使うと、name?: string | undefined と書かない限り、name?: string に対する { name: undefined } はコンパイルエラーになり、発生源で防げます。
Partial は浅い
Partial が省略可能にするのはトップレベルのプロパティだけです。ネストしたオブジェクトは、渡すなら完全でなければなりません:
index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.
editor プロパティは省略可能ですが、存在する場合の型は変わらず { fontSize: number; tabSize: number } です。設定のオブジェクト、APIのパッチ、テスト用のデータでは、すべての階層でプロパティを省略可能にしたいことがよくあります。それには再帰的な型が必要です。
DeepPartial: 再帰的な Partial
TypeScriptに組み込みの深い版はありませんが、数行で書けます。配列の要素を省略可能にすると [undefined] が許されてしまうので、関数と配列はそのままにします:
型は再帰的ですが、マージはそうではありません。オブジェクトのスプレッドも浅いので、applySettings は editor を手でマージしています。汎用の深いマージ関数は lodash(merge)などのライブラリにありますが、その型付けは上の型より難しくなります。
Required: Partial の逆
Required<T> はすべてのプロパティから ? を取り除きます。-? 修飾子で定義されていて、各プロパティの型から undefined も取り除きます:
パターンは Partial と同じで、向きが逆です。APIの利用者はゆるい設定を渡し、内部のコードはすべての値が存在することがわかっている Required 版を使います。このスプレッドにも上の更新関数と同じ穴があります。呼び出し側が明示的に port: undefined を渡すとデフォルト値が undefined で上書きされ、exactOptionalPropertyTypes が有効でない限りコンパイラーはそれを受け入れます。Required も Partial と同じように浅いものです。
一部のプロパティだけを省略可能または必須にする
Partial と Required はすべてのプロパティに適用されます。いくつかだけを変えるには、Pick と Omit で型を分けてから組み立て直します:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface Post {
id: number;
title: string;
body?: string;
}
type NewPost = PartialBy<Post, "id">; // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
| 型 | 効果 | 深く働くか |
|---|---|---|
Partial<T> | すべてのプロパティを省略可能に | いいえ |
Required<T> | すべてのプロパティを必須にし、undefined を取り除く | いいえ |
DeepPartial<T>(自作) | すべての階層で省略可能に | はい |
PartialBy<T, K>(自作) | キー K だけを省略可能に | いいえ |
Readonly<T> | すべてのプロパティを readonly に | いいえ |
よくある質問
TypeScriptの Partial は何をしますか?
Partial<T> は、T のすべてのプロパティを省略可能にした型を作ります。interface User { name: string; email: string } なら、Partial<User> は { name?: string; email?: string } なので、{}、{ name: "Ada" }、完全なユーザーのどれも正しい値です。
TypeScriptの Partial は深く働きますか?
いいえ。Partial が影響するのはトップレベルのプロパティだけです。Partial<T> の中のネストしたオブジェクトは、やはり完全でなければなりません。再帰的な版が必要なら、オブジェクト型のプロパティに自分自身を適用する DeepPartial<T> 型を書きます。
TypeScriptで Partial の逆は何ですか?
Required<T> です。すべてのプロパティから ? を取り除き、型から undefined も取り除くので、Required<{ port?: number }> は { port: number } です。-? 修飾子を使ったマップ型として定義されています。
一部のプロパティだけを省略可能にするには?
Omit、Pick、Partial を組み合わせます: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>。PartialBy<User, "email"> は、email だけを省略可能にし、ほかのプロパティはすべてそのままにします。
Partial の更新をマージするとプロパティが undefined になるのはなぜですか?
Partial<T> では、プロパティが undefined という値で存在することも許され、オブジェクトのスプレッドはそれをコピーするからです。{ ...user, ...{ name: undefined } } は name: undefined を持ちますが、TypeScriptは結果を User と型付けします。マージする前に undefined の値を取り除くか、exactOptionalPropertyTypes を有効にして明示的な undefined を拒否させましょう。