TypeScript で「ある型を許容しない」型を表現したい
はじめに
たとえば関数の引数として unknown
を受け付ける必要があるけれども、その引数に Promise
オブジェクトが渡されるのは関数の利用用途的に明らかに間違いであるといえる場合、Promise
オブジェクトを引数として渡せないように (Promise
を渡そうとするとコンパイルエラーとなるように) 関数の引数の型をいい感じに表現したくなるかと思います。
直感的には「特定の型の否定」を型 (英語で表現すると negated types) として表現できれば実現できそうなものではありますが、僕が知る限り「型の否定」を直接的に表現する文法・機能は現状の TypeScript には組み込まれていないはずです。(関連 issue: microsoft/TypeScript#4196 )
しかしながら、直接的な表現方法ではないにしても、現状の TypeScript の機能を用いれば比較的容易に「型の否定」に相当する型が表現できます。今回紹介するのは以下の記事で紹介されている、conditional types と never
を利用した実現方法になります。
実現方法
ある型を否定する型は、以下で表現できます。
type Not否定したい型<T> = T extends 否定したい型 ? never : T;
たとえば上記した Promise
を拒否する型は以下のように表せます。
type NotPromise<T> = T extends Promise<unknown> ? never : T;
function acceptUnknownButNotPromise<T>(obj: NotPromise<T>): T {
return obj;
}
// 以下の 4 つはコンパイルエラーにはならない
acceptUnknownButNotPromise('string');
acceptUnknownButNotPromise(123);
acceptUnknownButNotPromise(null);
acceptUnknownButNotPromise(undefined);
// しかし以下はコンパイルエラーになってくれる
const promise = new Promise<string>((resolve) => resolve('string'));
acceptUnknownButNotPromise(promise);
また否定したい型が複数存在する場合は、以下のように |
で挟んで列挙します。
type NotNullish<T> = T extends undefined | null ? never : T;
function acceptUnknownButNotNullish<T>(obj: NotNullish<T>): unknown {
return obj;
}
// 以下の 3 つはコンパイルエラーにはならない
acceptUnknownButNotNullish('string');
acceptUnknownButNotNullish(123);
acceptUnknownButNotNullish({});
// しかし以下はコンパイルエラーになってくれる
acceptUnknownButNotNullish(null);
acceptUnknownButNotNullish(undefined);
おわりに
そもそもの話として、TypeScript でコードを書いていて「型としてはほぼ unknown
だが、ある特定の型だけ拒否したい」という状況がそれほど頻繁にあるとは思いません (普段は unknown
など使わず、より具体的・限定的な型を指定すべきですし)。
しかしこの negated types は、valibot の parse()
のように unknown
で引数を受け付けるライブラリに対して関数を一枚ラップして特定の型を拒否する、みたいな場面では少なからず役に立つテクニックであり、頭の片隅に入れておいても損はないはずです。
Discussion