TypeScriptにはジェネリクスという仕組みがあります。ジェネリクスは異なる型で動作するコンポーネントや関数を一つの定義で扱うことを可能にする仕組みです。ジェネリクスを使うことで型の安全性を利用しつつ、コードを共通化するのが楽になります。
ジェネリクス (generics) | TypeScript入門『サバイバルTypeScript』
ジェネリクスは次のように使えます。
function chooseRandomly<T>(v1: T, v2: T): T {
return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>('勝ち', '負け');
chooseRandomly<number>(1, 2);
chooseRandomly(new Date('2016-04-08'), new Date('2021-01-01'));
string, number, Dateといった異なる型をTという一つの定義で扱っています。ジェネリクスが有効なのは型の流れが決まっている時です。例えば上記コードは次のように書いてもTypeScriptのエラーにはなりません。
function chooseRandomly(v1: string|number|Date, v2: string|number|Date): string|number|Date {
return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>('勝ち', '負け');
chooseRandomly<number>(1, 2);
chooseRandomly(new Date('2016-04-08'), new Date('2021-01-01'));
これと比べてジェネリクスを使って何がよくなったかというと引数のどちらかか返り値の型が片方が定まったら、もう片方も定まると型で示せるようになることが良くなりました。引数の一つがstringならば、もう一つの引数もstringであり、返り値もstringであると定まるのです。無駄に多くのケースを考える必要がなくなります。
その様なジェネリクスですがReact.memo等の高階関数を使うと、内部処理的には引数と返り値の型が全く同じであったとしてもジェネリクスが抜け落ちる場合があります。これは次のように起きます。
/**
* 与えられた関数をそのまま返す関数
* 型が与えられた関数の型そのものを返さない形
*/
function wrapper(fn: Function): Function {
// ここで何か色々やる
return fn;
}
const wrappedFn = wrapper(chooseRandomly);
wrappedFn('勝ち', 1); // ←TypeScriptの検査を通ってしまう
高階関数がfunction wrapperのようになっていれば、ジェネリクスは消えないのですが上記例のようにすると消えてしまいます。そうなるとジェネリクス付きの時ほど適した型でなくなってしまいます。これを手軽に対策する方法は次です。この方法は次のリンク先に書かれていた方法です。
[@type/react] Generic Props lost with React memo · Issue #37087 · DefinitelyTyped/DefinitelyTyped
function chooseRandomly<T>(v1: T, v2: T): T {
return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>('勝ち', '負け');
chooseRandomly<number>(1, 2);
chooseRandomly(new Date('2016-04-08'), new Date('2021-01-01'));
/** 与えられた関数をそのまま返す関数 */
function wrapper(fn: Function): Function {
// ここで何か色々やる
return fn;
}
// as typeof ラップ対象の関数 を追加する
const wrappedFn = wrapper(chooseRandomly) as typeof chooseRandomly;
wrappedFn('勝ち', 1); // ←TypeScriptの検査を通らない
as typeof 関数名で元の関数の型を引っ張って来るだけです。手軽に書けますし型を誤る場合も少ないです。