🤏
あるオブジェクト型から「そのプロパティのうちどれか1つ、しかもその1つだけしか持てない型」を作る
TypeScriptのバージョンは記事執筆時点における最新版(4.5.2)です。
OnlyOneOf<T>
まず補助的な型としてTのプロパティを全て持てないようにする型RejectAll<T>を作ります:
type RejectAll<T> = { [P in keyof T]?: never };
これを使ってタイトルにあるような型OnlyOneOf<T>が以下のようにして作れます:
type OnlyOneOf<T> = {
[P in keyof T]: RejectAll<Omit<T, P>> & Required<Pick<T, P>>;
}[keyof T];
何をやっているのかというとmapped typeによってTの各プロパティPに対し「そのPだけがrequiredであり、それ以外のプロパティは持つことができない(never)」型を生成した後、あらためてそれらをkeyof Tをindexとしたindex access typeを使って取り出してunion型にしている、というわけです。
これを使うと、例えば次のようにOptionsという型があったとき:
type Options = {
one: string;
two: number;
three: ThirdOption;
};
type ThirdOption = "3.1" | "3.2" | "3.3";
Optionsのプロパティのうちどれか1つ、しかもその1つだけしか選ぶことが許されない型を作ることができます:
type Choice = OnlyOneOf<Options>;
const firstChoice: Choice = {
one: "1",
};
const secondChoice: Choice = {
two: 2,
};
const thirdChoice: Choice = {
three: "3.3",
};
// @ts-expect-error: empty
const emptyChoice: Choice = {};
// @ts-expect-error: not assignable
const excessChoice: Choice = {
one: "1",
two: 2,
};
楽しいですね。
このようにmapped typeを使って各プロパティにおける変換を行った上でそれらを[keyof T]によるindex access typeで取り出してやるイディオムは汎用性が高く、色々な応用例を作れる気がします。
exactOptionalPropertyTypesについて補足
実はTypeScriptのデフォルトの設定では、上記の例において禁止したはずのプロパティでも値をundefinedにすれば持たせることが可能です。つまり
const firstChoice: Choice = {
one: "1",
two: undefined,
};
のようなことが許されます。ただしTypeScript 4.4で追加されたexactOptionalPropertyTypesオプション[1]を有効にすればoptionalなプロパティにexplicitにundefinedを渡すことができなくなるので、真に1つだけのプロパティしか選ぶことが許されなくなります。
Discussion