1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Typescriptで「全部未指定」または「どれか1つが指定されている」型を定義する

Last updated at Posted at 2023-06-09

この記事について

業務でタイトル通りの型を作りたいケースが発生し割と試行錯誤したのでその内容を共有をしたいと思います。
同じようなものが作りたくて困っている人のお役に立てたら幸いです。

やりたかったこと

とある画面のオプションがあり、こんな感じの内容でした。
1つは「ログアウトしていても有効にする」というオプション
もう1つは「管理者権限でログインしているアカウントのみ有効」というオプションです。

{
  logout: 1 | 0; // ログアウトしてても有効にするオプション
  admin: 1 | 0; // 管理者権限でログインしているアカウントのみ有効にするオプション
}

見てもらうとわかると思いますが上記の2つは排他関係にありどちらかが有効な場合、もう一つは無効になっている必要があります。
なので「どちらか片方だけしか指定することはできない」が表現された型を作りたいと思っていました。
加えてシステム構築時にこれらのオプションは存在しておらず後から追加されたため、他の箇所に影響が出ないように「いずれも指定されていない」というのも型として表現したいなと思っていました。

まずは結論

結果的に以下のようにすると上記の型を表現することができました!

// 全てのプロパティを持てなくする汎用型
type RejectAll<T> = { [P in keyof T]?: never };

// `定義されているプロパティの中でどれか1つは必須`を表現する汎用型
type OnlyOneOf<T> = {
  [P in keyof T]: RejectAll<Omit<T, P>> & Required<Pick<T, P>>;
}[keyof T];

// どれか1つは必須の対象となるプロパティ
type RequireWhichOneOptions = {
  logout: 1 | 0;
  admin: 1 | 0;
};

// いずれも指定されていない or どれか1つが指定されている 以外は許さない
type WhichOneOrNothing = OnlyOneOf<RequireWhichOneOptions> | RejectAll<RequireWhichOneOptions>;

解説

まず、以下の部分ですが こちら の記事を参考にさせていただきました。(投稿者の方、本当に助かりました!!ありがとうございました!)

// 全てのプロパティを持てなくする汎用型
type RejectAll<T> = { [P in keyof T]?: never };

// `定義されているプロパティの中でどれか1つは必須`を表現する汎用型
type OnlyOneOf<T> = {
  [P in keyof T]: RejectAll<Omit<T, P>> & Required<Pick<T, P>>;
}[keyof T];

まずはジェネリクスで引き渡されたオブジェクトの全てのプロパティを持つことを不可とする型を作ります。

type RejectAll<T> = { [P in keyof T]?: never };

もし、以下の様にすると

type Hoge = RejectAll<{
  logout: 1 | 0;
  admin: 1 | 0;
}>;

logoutadmin もどちらも持てない」という型になります。

これだけだと「誰得...?」な状態になってしまうので次に上記を利用した別の型を定義します。

type OnlyOneOf<T> = {
  [P in keyof T]: RejectAll<Omit<T, P>> & Required<Pick<T, P>>;
}[keyof T];

RejectAll<Omit<T, P>> は「当該のプロパティ以外を定義不可能とする」
Required<Pick<T, P>> は「当該のプロパティを定義必須とする」となり、
上記を組み合わせた結果「当該のプロパティしか持てない」型になります。

RejectAll<Omit<T, P>> & Required<Pick<T, P>>

そして最後の [keyof T] の部分でUNION型にしている様です。
その結果以下のような型を直接指定した場合と同義の「いずれか1つは定義が必須」という型を作り出すことができます。

{ logout: 1 | 0; admin: never; } | { logout: never; admin: 1 | 0; }

ただし、今回のケースの場合、上記だけでは「いずれも定義されない場合」がカバーできていません。
なので、最初に定義した RejectAll を利用して「いずれも定義されていない」型を定義し上記の型とUNIONにします。

// いずれも指定されていない or どれか1つが指定されている 以外は許さない
type WhichOneOrNothing = OnlyOneOf<RequireWhichOneOptions> | RejectAll<RequireWhichOneOptions>;

// 多分こんな感じになってて「未定義かどれか1つを定義」以外はエラーになる
{ logout: 1 | 0; admin: never; } | 
{ logout: never; admin: 1 | 0; } | 
{ login: never; admin: never; }

こうすることでやりたいことが型で表現でき、処理に頼らずに正当性チェックをトランスパイラに任せることができる様になりました!

まとめ

image.png

最後までお読みいただきありがとうございました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?