TypeScript ジェネリクス


前回に引き続きTypeScriptを学ぼうシリーズ。

前回まではこちら。

https://www.sunapro.com/typescript%e3%81%ae%e5%9e%8b%e5%9f%ba%e6%9c%ac/

https://www.sunapro.com/typescript-%e3%82%a4%e3%83%b3%e3%82%bf%e3%83%bc%e3%83%95%e3%82%a7%e3%83%bc%e3%82%b9/

https://www.sunapro.com/typescript-%e5%bf%9c%e7%94%a8%e7%9a%84%e3%81%aa%e5%9e%8b/

色々まとめてきたけど一旦ここで最後。
今回はジェネリクスについて色々まとめる。

目次

ジェネリクスとは

他の特定の型と結合された型

Array<string>Promise<number>などの<>の部分のこと。
ArrayやPromiseだけでなくその中身の型まで指定する時に使うもの。

ただこれは基本的な使い方で、もっと便利な使い方があるのでこれから見ていく。

ジェネリクスはインターフェース同様、JavaScriptにはなくTypeScriptにしかない

利用目的

  • TypeScriptの型安全性を高めることができる
  • 自動補完などの開発サポートを向上させることができる

2点目は結構便利だと思っている。
どんな感じかと言うと、以下のような場合にnamesはnumberなのでsplitメソッドは使えない。
よってここでエラーを表示させることができる

const names: Array<number> = []; // number
names[0].split(' ') //Error

Generic関数

推論された型がobjectなどの場合は非常に曖昧なので、そのobjectに対してあるかどうかわからないプロパティを使うのはアラートが出る(6行目)。

function merge(objA: object, objB: object) { // 型推論でreturnされる型はobjectになる
  return Object.assign(objA, objB); // objA + objBになる
}

const mergedObj = merge({ name: 'suna' }, { age: 30 }); // mergedObjの型推論は単なるobject
mergedObj.age; // Property 'age' does not exist on type 'object'.

このような場合にジェネリクスで交差型にすることでオブジェクトの中身まで型を定義してあげるとよい。

以下ではTとUが違う型である可能性を示唆している(同じ型でも良い)。
TはTypeのT, Uはその次のアルファベットという慣習らしいが、特に決まりはない。

function merge<T, U>(objA: T, objB: U) { // 推論結果はT & Uになる
  return Object.assign(objA, objB); // objA + objBになる
}

const mergedObj = merge({ name: 'suna' }, { age: 30 }); // mergedObjはnameとageをプロパティに持つobjectと推論される
mergedObj.age; // No alert。ageがあることがわかっている。

ジェネリクスを使うことによって単なるオブジェクトではなく、オブジェクトの中身まで推論の材料にしてくれる。
結果、mergedObjの型推論は単なるオブジェクトではなく、引数に取ったnameとageプロパティを持つオブジェクトだと推論される。({ name: string } & { age: number }の交差型)

これによって6行目の時点でageを持つことがわかっているので何も警告が出ない。

ジェネリクス型に制約を追加する

ただしここで1つ問題がある

TとUはそれぞれ別の型である可能性があるが、その詳細な中身までは規定されていない。
よって以下のようにするとエラーにはならない。

function merge<T, U>(objA: T, objB: U) {
  return Object.assign(objA, objB); // objBはオブジェクトではないのでこれはエラーにはならないが単にobjAになる
}

const mergedObj = merge({ name: 'suna' }, 30); // mergedObjは{ name: 'suna' }だけになる

エラーにはならないが、mergeObjはマージされていないものになる。
理想としてはこの時点でマージできない場合はTypeScriptにエラーを出して欲しい。
そのためには、TとUの型をもう少し詳細に規定する必要がある。

これはextendsを使うことで実装できる。
以下の例は2つの引数としてオブジェクトを受け取り、それらがそれぞれ異なるオブジェクトである可能性があることを示している。
つまりTとUはオブジェクトであり、それぞれの型が異なる可能性がある

function merge<T extends object, U extends object>(objA: T, objB: U) {
  return Object.assign(objA, objB);
}

const mergedObj = merge({ name: 'suna' }, 30); // 30の部分がオブジェクトじゃないのでエラーになる

extendsではinterfaceやtypeも渡すことができる。

そのため、特定のプロパティがあることを明示することもできる。

interface Length {
  length: number
}

function count<T extends Length>(num: T) {
  return num.length; // Lengthの中にlengthプロパティがなかったらエラー
}

これはLengthというインターフェースの中にlengthメソッドがなければエラーになる

keyof

オブジェクトとkeyを使って操作をする際に、そのオブジェクトの中に操作したいkeyがあるかどうかを判別する際に使用するもの。

以下のようにobjectとkeyを渡した時にそのobjectに該当のkeyがあることを保証していないのでobj[key]はエラーになる

function test<T extends object, U>(obj: T, key: U) {
  return obj[key]; // Error
}

test({}, "name");

よって引数のobjにはkeyで示されたプロパティがあることを示す必要がある。

以下のようにジェネリクスの中でextends keyofを使う。

function test<T extends object, U extends keyof T>(obj: T, key: U) {
  return obj[key]; // No Error
}

test({}, "name"); // Error

これで関数の中身はエラーにならなくなったが、関数の呼び出し部分でエラーが得られる。
これはobjの中にnameプロパティがないから。

これは以下のようにすればエラーがなくなる

test({ name: 'suna' }, 'name');

このようにしてオブジェクトにないkeyを使った操作を未然に防ぐことができる。

ユーティリティ

TypeScriptにはいくつかのユーティリティが存在していてメジャーなのはPartialとReadonly

Readonlyはその名の通り読み取り専用にする場合に使うので省略

Partialの使い方

特定の型の全てのプロパティを省略可能(| undefined)にすることができる。

以下具体例。

以下のような場合にはtitleプロパティがないというエラーが出る。
これはgoalの{}にtitleプロパティが存在しないからである。

interface Goal {
  title: string;
}

function createGoal(title: string): Goal {
  let goal = {};
  goal.title = title; // goal.titleのtitle部分にError
  return goal; // Error
}

よって次のように型を定義すると今度はインターフェースのGoalでtitleプロパティがあるため、{}の中にはtitleプロパティが必要なのにもかかわらず存在しないというエラーが出る

interface Goal {
  title: string;
}

function createGoal(title: string): Goal {
  let goal: Goal = {}; // {}にtitleプロパティがないというError
  goal.title = title;
  return goal;
}

このような場合に次のようにPartialを使うと解決する。

Partialを使うと省略可能になるので{}の中にtitleがなくても問題ない。

ただし、最終的に得られるgoalの型はPartialになっているのでこれをGoalに変換する必要がある。

interface Goal {
  title: string;
}

function createGoal(title: string): Goal {
  let goal: Partial<Goal> = {};
  goal.title = title;
  return goal as Goal; // Partialになっているので明示的にGoalに変換
}

おわりに

ジェネリクスはReact使ってる時にも何度か見たからしっかり使い方覚えたい。

長かったTypeScriptまとめもこれで一区切りなのでいったん終了とする。

先日社内のミーティングでこれからのプロジェクトにはTypeScriptを使うことが決定したので、あとは使いながら覚える。
コード補完が効くのでTypeScript使って爆速でコーディングできるようになりたい。

参考

TypeScriptの型基本についてはこちら

https://www.sunapro.com/typescript%e3%81%ae%e5%9e%8b%e5%9f%ba%e6%9c%ac/

インターフェースについてはこちら

https://www.sunapro.com/typescript-%e3%82%a4%e3%83%b3%e3%82%bf%e3%83%bc%e3%83%95%e3%82%a7%e3%83%bc%e3%82%b9/

TypeScriptの応用的な方についてはこちら

https://www.sunapro.com/typescript-%e5%bf%9c%e7%94%a8%e7%9a%84%e3%81%aa%e5%9e%8b/

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次