問題点
型を色々と合併したり交差した後、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>
の K
が keyof 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
を定義することで落ち着きました。