【TypeScript】Generics型で動的に型を決める
どうもフロントエンドエンジニアのoreoです。
今回はTypeScriptのGenerics型(ジェネリクス型)について整理したいと思います。
1 Generics型とは?
Generics型とは、使用されるまで型が確定せず、使用された時に動的に型が決まるものです。「総称型」とも呼ばれます。動的に型が決まる為、型安全を保持したままコードを共通化したい場合などに役に立ちます。
書き方としては、Generics型を定義する際に任意の型エイリアスで型を宣言し、それを利用する側では< >に型を指定します。
具体的には、以下のように、型エイリアスをTとして、Generics型のインターフェイスvalueObjを定義します。valueObj<string>とすると型は{ value: stirng }と推論され、valueObj<number>とすると型は{ value: number }と推論されます。
//Generics型のインターフェイスの定義
interface valueObj<T> {
  value: T;
}
//型はconst stringObj:{ value: stirng }と推論される
const stringObj: valueObj<string> = {
  value: "test",
};
//型はconst numberObj:{ value: number }と推論される
//valueはnumber型でなければならないので「 型 'string' を型 'number' に割り当てることはできません」とエラーが発生
const numberObj: valueObj<number> = {
  value: "test", 
};
型エイリアスは、慣習的にtypeからTとすることが多く、複数のGenerics型を使用する場合は、アルファベット順にT、U、、とする模様です。もちろん、T以外でもOKです。
2 具体例
ユーザー情報のオブジェクトを結合して返すgetUserInfo関数を例として考えます。
userInfoとして名前{ name: "nobita" }と年齢{ age: 10 }を結合したオブジェクトを宣言します。
名前を出力するためにconsole.log(userInfo.name)とすると、エラーProperty 'name' does not exist on type 'object'となり、nameプロパティにアクセスできません。これは、TypeScriptでは、userInfoをobject型と推論している為、nameプロパティがないと怒られます。
function getUserInfo(obj1: object, obj2: object) {
  return { ...obj1, ...obj2 };
}
//型はconst userInfo: objectと推論される
const userInfo = getUserInfo({ name: "nobita" }, { age: 10 });
console.log(userInfo);  //{name: 'nobita', age: 10} と出力
console.log(userInfo.name); //Property 'name' does not exist on type 'object'
そこでGenerics型を使用します。下記のようにobj1 : T、obj2 : UとGenerics型を定義すると、getUserInfo関数の返り値は、T & UつまりTとUのintersection型(交差型)と推論されます。これによりuserInfoは、{name: string;} & {age: number;}型と推論されるようになり、console.log(userInfo.name)では、エラーなくnobitaが出力されるようになります。
function getUserInfo<T, U>(obj1: T, obj1: U) {
  return { ...obj1, ...obj2 };
}
//型はconst userInfo: {name: string;} & {age: number;}と推論
const userInfo = getUserInfo({ name: "nobita" }, { age: 10 });
console.log(userInfo); //{name: 'nobita', age: 10} と出力
console.log(userInfo.name); //nobita と出力!!!
Generics型ではなく、userInfoで型キャストしてあげても、エラーは解消されますが、冗長になりますね。
3 制約をつける
 3-1 extendsキーワード
Generics型にextendsをつけることで、「Generics型はextendsで指定した型を満たさなければならない」という制約をつけることができます。2の例で使用したgetUserInfo関数で具体例を説明します。
2の状態では、TとUにどのような型でも渡すことが可能です。そこでTとUにextends objectとをつけると、TとUがobject型でなければならないという制約をつけることができます。この状態で引数にobject型以外を渡すと下記のようにエラーとなります。
function getUserInfo<T extends object, U extends object>(obj1: T, obj2: U) {
  return { ...obj1, ...obj2 };
}
//型 'number' の引数を型 'object' のパラメーターに割り当てることはできません
const userInfo = getUserInfo({ name: "nobita" }, 10);
 3-2 keyof型演算子
keyofは、object型からプロパティ名を型として返す型演算子です。
下記のようにkeyofとextendsを組み合わせることで、Uはobject型であるTのプロパティ名を型として持たなければならなくなり、プロパティ名以外のkeyを渡すとエラーとなります。
function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
  return obj[key];
}
//型 '"age"' の引数を型 '"name"' のパラメーターに割り当てることはできません。
console.log(getValue({ name: "doraemon" }, "age"));
最後に
共通化する際に、非常に便利なツールですね。OSSなどでは多用されているので、慣れていきたいです!
参考
ジェネリクス (generics) | TypeScript入門『サバイバルTypeScript』
ConditionalTypes I/O - TypeScript3.4 型の強化書 -(電子版) - takepepe - BOOTH

Discussion
オーガニックで辿り着きました笑
ようこそですw