TypeScriptで「このキーのうち1つは存在すること」っていう型を定義する
ChatGPTに教えてもらったことのまとめ。 ChatGPT - Omit複数プロパティ
目的
type AuditLevel = "critical" | "high" | "moderate" | "low";
っていうUnion型があって、この型をキーに取るRecord型を作りたい。ただし、4つのうち1つは入っているようにしたい。
Recordを使った基本
type AuditResult = Record<AuditLevel, number>;
ってやると const result: AuditResult は、AuditLevelのキーを「全て」持っていなければならなくなる。
Partial使った場合
さっきのRecordの型にPartialを使って
type AuditResult = Partial<Record<AuditLevel, number>>;
ってやると一部分のキーを持っていてもOKになる。…が、この場合、const result: AuditResult = {} ; でもOKになってしまう。1つはキーを持って欲しいのである。
「1つだけキーがあるオブジェクトのユニオン型」を作る
「1つは入っているようにしたい」…というのを型で表すと
{ critical: number } | { high: number } | { moderate: number } | { low: number }
である。
この型を作るために、一旦以下の型を作る。
type T = { critical: { critical: number }; high: { high: number }; moderate: { moderate: number }; low: { low: number }; };
これは型操作のMapped Typeを使って以下のように作れる。
type T = { [K in AuditLevel]: { [P in K]: number }; };
これは以下のようにPをkeyにして書くとわかりやすい。
type T = { [K in AuditLevel]: { [key in K]: number } ];
あるいはRecordを使った方がより読みやすい。
type T = { [K in AuditLevel]: Record<K, number>; };
このMapped Type Tのうち、欲しいキーだけ抜き出したかったら
type Tpick = T["high"];
とすればよい。さらに「キーのうちどれか1つ」っていうのはUnion型なので
type Tpick = T["high"|"critical"];
ってすればよい。で、この「キーのうちどれか1つ」のUnion型ってのは結局AuditLevel型なので
type Tpick = T[AuditLevel];
となる。
ただ、T[AuditLevel] のままだと「1つだけキーを持つ型のユニオン」になってしまい、
「複数キーがある」オブジェクトはエラーになる。
なので、他のキーを optional にするために & Partial<Record<AuditLevel, number>> を加えて
type Tpick = T[AuditLevel] & Partial<Record<AuditLevel, number>>;
というようにする。
結論として
type AuditResult = { [K in AuditLevel]: Record<K, number>}[AuditLevel] & Partial<Record<AuditLevel, number>>;
となる。
汎用的にするためのユーティリティ型を作る
こんな感じでジェネリクスを使った型定義をする。
type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T] & Partial<T>;
を使うとさっきのは
type AuditResult = Record<AuditLevel, number>; type AtLeastOneAuditResult = AtLeastOne<AuditResult>;
という型定義になる。新しく型を作らなくてもよい。
まとめ
type AuditLevel = "critical" | "high" | "moderate" | "low"; type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T] & Partial<T>; type AtLeastOneAuditResult = AtLeastOne<Record<AuditLevel, number>>;