tohokuaikiのチラシの裏

技術的ネタとか。

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>>;