【TypeScript】Generics型で動的に型を決める

13278 ワード

どうもフロントエンドエンジニアの です。

今回は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型を使用する場合は、アルファベット順にTU、、とする模様です。もちろん、T以外でもOKです。

2 具体例

ユーザー情報のオブジェクトを結合して返すgetUserInfo関数を例として考えます。

userInfoとして名前{ name: "nobita" }と年齢{ age: 10 }を結合したオブジェクトを宣言します。

名前を出力するためにconsole.log(userInfo.name)とすると、エラーProperty 'name' does not exist on type 'object'となり、nameプロパティにアクセスできません。これは、TypeScriptでは、userInfoobject型と推論している為、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 : Tobj2 : UとGenerics型を定義すると、getUserInfo関数の返り値は、T & UつまりTUの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の状態では、TUにどのような型でも渡すことが可能です。そこでTUextends objectとをつけると、TUobject型でなければならないという制約をつけることができます。この状態で引数に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型からプロパティ名を型として返す型演算子です。

下記のようにkeyofextendsを組み合わせることで、Uobject型である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