🤏

あるオブジェクト型から「そのプロパティのうちどれか1つ、しかもその1つだけしか持てない型」を作る

2021/12/02に公開

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つだけのプロパティしか選ぶことが許されなくなります。

脚注
  1. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#exact-optional-property-types---exactoptionalpropertytypes ↩︎

GitHubで編集を提案

Discussion