TypeScript で「ある型を許容しない」型を表現したい

2024/04/10に公開

はじめに

たとえば関数の引数として unknown を受け付ける必要があるけれども、その引数に Promise オブジェクトが渡されるのは関数の利用用途的に明らかに間違いであるといえる場合、Promise オブジェクトを引数として渡せないように (Promise を渡そうとするとコンパイルエラーとなるように) 関数の引数の型をいい感じに表現したくなるかと思います。

直感的には「特定の型の否定」を型 (英語で表現すると negated types) として表現できれば実現できそうなものではありますが、僕が知る限り「型の否定」を直接的に表現する文法・機能は現状の TypeScript には組み込まれていないはずです。(関連 issue: microsoft/TypeScript#4196 )

しかしながら、直接的な表現方法ではないにしても、現状の TypeScript の機能を用いれば比較的容易に「型の否定」に相当する型が表現できます。今回紹介するのは以下の記事で紹介されている、conditional typesnever を利用した実現方法になります。

https://catchts.com/type-negation

実現方法

ある型を否定する型は、以下で表現できます。

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);

上記コードを TS Playground で開く

また否定したい型が複数存在する場合は、以下のように | で挟んで列挙します。

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);

上記コードを TS Playground で開く

おわりに

そもそもの話として、TypeScript でコードを書いていて「型としてはほぼ unknown だが、ある特定の型だけ拒否したい」という状況がそれほど頻繁にあるとは思いません (普段は unknown など使わず、より具体的・限定的な型を指定すべきですし)。

しかしこの negated types は、valibot の parse() のように unknown で引数を受け付けるライブラリに対して関数を一枚ラップして特定の型を拒否する、みたいな場面では少なからず役に立つテクニックであり、頭の片隅に入れておいても損はないはずです。

Discussion