Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

引数は固定だけど返り値でジェネリクスを使いたいケースがある

In short:

  • 関数が reference を返す場合(useRef の返り値など)は返り値のみにジェネリクスを使っても問題ない
  • fetch のような返り値が引数の内容によって決まるケースでも(型安全性は犠牲になるが)使うことがある。

ジェネリクスの一般的な用途

そもそもジェネリクスの目的は 「引数の型だけちょびっと異なる型を上手く扱いたい」 というのが原点です

const id = <T>(x: T) => x;
const toString = <T>(x: T) => `${x}`;

id 関数も toString 関数も、引数の型として number も string も受け付けています。

このように、ジェネリクスは引数の型を可変にするために使われるのが一般的 です。

引数にジェネリクスを使わないパターン

引数にジェネリクスを使わないパターンは ごく一部の例外を除いて存在しません (その例外についてが本題なので後述します)。

例えば常に例外を投げて失敗する async 関数を考えます

const throwError = <T>(): Promise<T> => {
    throw new Error();
};

型検査も通りますし、使うこともできます。ただ、Promise<T> の代わりに Promise<never> と書いても同じです。

const throwError = (): Promise<never> => {
    throw new Error();
};

このように、返り値にジェネリクスを使わない形に書き換えられるケースがほとんどです。

返り値にのみジェネリクスを使うケース: 返り値が reference のとき

見てきたように基本的に返り値にだけジェネリクスを使うケースはありませんが、レアな例外の一つが reference を返すケースです。React ユーザーであればすぐピンとくると思います。

たとえば const divRef = useRef<HTMLDivElement>(null) のようなコードを書いたことがありませんか? この場合、引数は null ですが、返り値が RefObject<HTMLDivElement> です。まさに引数にはジェネリクスを使わないのに返り値にジェネリクスを使うケースです。

このように、状態が変化しうるものが返り値のときは、返り値にだけジェネリクスが現れます。

返り値にのみジェネリクスを使うケース: fetch 関数

fetch 関数も、返り値にのみジェネリクスを使う例外の一つです。

const fetchAPI = <T>(endpoint: string): Promise<T> =>
  fetch(endpoint).then((res) => res.json());

ただし、この場合は実はジェネリクスを使わなくても書けます。

const fetchAPI = (endpoint: string) =>
  fetch(endpoint).then((res) => res.json());

この場合、Promise<any> が返ってきます。つまり、any を T に変更しているのです。実際には any なので当然、型安全性を損ないます。ただ、「API は滅多に変わらないから type guard なしでキャストしてしまおう」という判断は実際のプロダクトでも十分考えられます(type guard を手動で管理しようとすると結構な労力になるので…)

このように型安全性を多少犠牲にしつつ利便性をとる場合にも使われることがあります。当然ですが多用すべきではありません。

まとめ

ジェネリクスが返り値に現れた場合、それらはほとんどの場合 unknown あるいは never で置き換えることができます。reference を返すケースを除けば、型安全性のためにも返り値だけにジェネリクスを使うのはお勧めしません。