🤏
あるオブジェクト型から「そのプロパティのうちどれか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