0
1

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.

Omit<T,K> はやりすぎると any になる

Last updated at Posted at 2022-07-01

問題点

型を色々と合併したり交差した後、Omit でプロパティの消去を行うと空になってしまうことがあります。そして、Omit<> を使用して全てのプロパティが消失すると any として振舞ってしまいます。

例えば、REST API があって、 API 型を定義して、こんな感じで使ったりします。

/** リクエストの共通パラメータ */
type REQEST_COMMON = {
  random: number;
};

/** レスポンスの共通パラメータ */
type RESPONSE_COMMON = {
  random: number;
  result: number;
};

/** SET_VALUE と GET_VALUE という API のリクエストとレスポンスのパラメータ */
type API = {
  SET_VALUE: {
    REQUEST: REQEST_COMMON & { selector: "SET_VALUE"; value: number; };
    RESPONSE: RESPONSE_COMMON & { selector: "SET_VALUE"; };
  };
  GET_VALUE: {
    REQUEST: REQEST_COMMON & { selector: "GET_VALUE"; };
    RESPONSE: RESPONSE_COMMON & { selector: "GET_VALUE"; value: number };
  };
};

/**
 * 以下のポリシーで selector と random を Omit する
 * - selector は引数として与えられるので不要
 * - random は共通パラメータなので callApi に処理を集約するため不要
 **/
function callApi<T extends keyof API>(
  selector: T,
  parameter: Omit<API[T]["REQUEST"], "selector" | "random">
): Promise<API[T]["RESPONSE"]> {
  return new Promise<API[T]["RESPONSE"]>((resolve, reject) => {
    /** 何らかの非同期処理(http postする的な) */ 
  });
}

/** 呼び出し側のコード */
callApi("SET_VALUE", {value: 123})
.then(re => {});

callApi("GET_VALUE", {})
.then(re => {})

ここまでは良いのですが、問題は下記のコードが許されてしまうことです。

callApi("GET_VALUE", {value: 123})
.then(re => {})

GET_VALUE では value というプロパティを指定することはできない筈ですが、何故かエラーが発生せず、間違いに気付きにくいです。
寛容な REST API であれば、無駄なプロパティを設定しても受け入れてくれるかもですが、厳格 REST API だとパラメータチェックでエラーになるかも知れません。

これが何とかならないのかというのが、今回の問題です。

考察

問題を整理

「ことの発端」ではグチャグチャとコードを書いていますが、問題を単純化すると次のようなケースになります。

type X = {
    x: number;
};

/** プロパティを設定することはできない筈だがエラーにならない */
const x: Omit<X, "x"> = {
  x: 123,
  hoge: "fuga"
};

ネットを漁る

Omit<T,K>Kkeyof T じゃないのは何でだ!的な熱い議論がされているようですが、Omit によって空っぽ({}型)になった場合について私は記事を見つけることができませんでした。

Typescript って静的解析なんだから keyof T にすべきじゃないかと思いますけど、ここは互換性を考慮しているのかな?
まぁ、そうしたいなら、自分で作れば良いって感じですかね。

{}型って any なの?

{} 型 は any のように振舞まうことが確認されます。

const x: {} = {
   a: 1
};

で、この {}型は TypeScript の型としてどういう取り扱いなのかを確認してみました。

type _1 = {} extends any ? 1 : 0; /** _1 は 1 */
type _2 = any extends {} ? 1 : 0; /** _2 は 0 | 1  */

これで分かることは。。。

  • {} 型は any からの派生である
  • any{} 型からの派生でもあるし、そうとも言い切れない

_2 の結果についてはシュレーディンガー的な感じで想定外でしたが、extends {} は使えそうな予感がします。

解決案

と、まぁ、浅い考察の結果 Drop というジェネリクス型を作ってみました。

/**
 * EmptyType.ts のように別ファイルにして
 * EmptyType のみ export することで EMPTY_SYMBOL を隠蔽することが望ましい
 **/
const EMPTY_SYMBOL = Symbol();
type EmptyType = {[EMPTY_SYMBOL]?: never};

type Drop<T, K extends string | number | symbol> =
  {} extends Omit<T, K> ? EmptyType : Omit<T, K>;

type X = {
  x: number;
};
type Y = {
  y: number;
};

const _1: Drop<X, "x"> = {
  x: 1 /** <-- エラー */
}

const _2: Drop<X & Y, "x"> = {
  y: 1
}

const _3: Drop<X & Y, "x"> = {
  y: 1,
  x: 1 /** <- エラー */
}

const _4: Drop<X & Y, "x" | "y"> = {
  y: 1 /** <- エラー */
}

const _5: Drop<X & {}, "x"> = {
  x: 1 /** <- エラー */
}

当初{}型の表現は{"\0"?: undefined}だったり、{[Symbol.iterator]?: undefined}だったりしましたが、最終的に非公開のsymbolを生成してからEmptyTypeを定義することで落ち着きました。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?