【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 制約をつける
extends
キーワード
3-1 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);
keyof
型演算子
3-2 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