こんにちは。KOUKIです。
TypeScriptのジェネリクスについて、概要や使い方をまとめました。
<目次>
環境構築
以下の記事で作成したプロジェクトを使います。
※TypeScriptが動けば問題ありません
ジェネリクス(Generics)とは
2: 自動保管等の開発サポートを向上することができる
3: 型に柔軟性を持たせることができる
ジェネリクスは、平たく言うと「型」です。ジェネリック型です。
他の特定の型と結合されてできています。
配列を元に考えるとわかりやすいです。
1 2 |
// 型 -> onst color: string[] const color = ["red", "blue", "yellow"]; |
上記のコードは、「string型の配列」です、
配列であるため、Array型を型として指定することができるはずです。
1 |
const color: Array = ["red", "blue", "yellow"]; |
しかし、上記のコードでは、次のエラーが発生します。
1 2 |
// ジェネリック型'Array<T>'には1個の型引数が必要です。 Generic type 'Array<T>' requires 1 type argument(s). |
TypeScriptはArray型が格納する値が、どのような型かをチェックしません。そのため「どんな型であるか」を指定する必要があります。
1 2 3 4 |
// 配列はstringのtypeが格納されることをTypeScriptに伝える const color: Array<string> = ["red", "blue", "yellow"]; // 以降、stringのメソッドが使える color.length; |
このように、ジェネリック型は追加の型情報を提供できる型です。
ジェネリクスは、TypeScriptの機能であるため、JavaScriptには存在しません。ただし、ジェネリクスの概念は、JavaやC#など他の言語にも存在します。
配列を例に説明しましたが、配列だけに指定できるわけではなく、関数やクラスなどにも指定できます。
ジェネリック関数
最初にジェネリックを使った関数について、触れます。
まずは、ジェネリック無しの関数を定義しましょう。
1 2 3 4 5 6 |
function merge(objA: object, objB: object) { return Object.assign(objA, objB); } const mergeObj = merge({ name: "selfnote" }, { age: 31 }); mergeObj.name; |
上記のコードでは、オブジェクトを引数としてmerge関数に渡しています。merge関数では、それぞれのプロパティをマージしたオブジェクトを呼び出し元に返しています。
この状態で、プロパティの一つである「name」にアクセスすると以下のエラーが発生します。
1 |
Property 'name' does not exist on type 'object'. |
TypeScriptは、mergeObjの中に「name」プロパティが存在していることをチェックすることができないようです。
関数に渡しているobject型は、型としては非常に曖昧なもので、オブジェクトであれば何でも渡せてしまいます。そのため、このままだとTypeScriptはオブジェクトに存在するプロパティを判別することができません。
1 2 |
// TypeScriptは、単純にobjectが返されることしかわからない function merge(objA: object, objB: object): object |
この場合、ジェネリックスを利用すると便利です。
先ほども説明しましたが、ジェネリックス型は「後から型を追加できる」型です。
merge関数を以下のように修正しましょう。
1 2 3 4 5 6 7 |
// T, Uは、ジェネリック型の慣習 function merge<T, U>(objA: T, objB: U) { return Object.assign(objA, objB); } const mergeObj = merge({ name: "selfnote" }, { age: 31 }); mergeObj.name; |
不思議に感じるかもしれませんが、これでエラーが消えます。
ジェネリックスをつけた場合は、merge関数は曖昧なobject型ではなく、明確に交差型(objA, objB)を返すことを理解します。
1 2 |
// ジェネリックスの場合、T & U(交差型)を返すとTypeScriptが理解する function merge<T, U>(objA: T, objB: U): T & U |
面白いのは、この型は、呼び出し元の引数の指定で決まるということです。
つまり、以下のように「柔軟な」コードがかけます。
1 2 3 4 5 6 7 8 9 10 |
function merge<T, U>(objA: T, objB: U) { return Object.assign(objA, objB); } const mergeObj1 = merge({ name: "selfnote" }, { age: 31 }); mergeObj1.name; // プロパティを帰る const mergeObj2 = merge({ job: "develper" }, { company: "Google" }); mergeObj2.job; |

呼び出した時に、初めて型が決まりますね。
ジェネリックスに制約を追加する
ジェネリックスにより柔軟なコードを書くことができるようになりましたが、逆に柔軟すぎて簡単にバグを生産できてしまいます。
1 2 3 4 5 6 7 8 |
// T, Uは、ジェネリック型の慣習 function merge<T, U>(objA: T, objB: U) { return Object.assign(objA, objB); } // 31(number)を渡してもエラーにならない const mergeObj1 = merge({ name: "selfnote" }, 31); mergeObj1.name; |
上記は、第二引数に31(number)を渡してますが、エラーを発生させません。これは問題です。
ジェネリックス型は、「extends」キーワードを使用することで、制約を追加することができます。
1 2 3 4 5 6 |
// 引数はobject型であると制約を追加する function merge<T extends object, U extends object>(objA: T, objB: U) { return Object.assign(objA, objB); } const mergeObj1 = merge({ name: "selfnote" }, 31); |
制約を追加したため、エラーを検出できるようになりました。
1 |
Argument of type '31' is not assignable to parameter of type 'object'. |
尚、制約にはnumberやstring、クラス、ユニオン型など様々な型を指定できます。
ジェネリック型は曖昧さを残す
ジェネリック型の概念は少し難しく感じるかもしれません。しかし、慣れるとかなり便利に使えます。
例えば、ジェネリック型は関数の定義時に型のタイプを「決定づけない」ことが可能です(曖昧さを残す)。
例えば、パラメータをカウントする関数があるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// lengthを定義したinterfaceを用意する interface Lengthy { length: number; } function countParameter<T extends Lengthy>(p: T) { // Tは、lengthプロパティをもつLengthyを実装したので、p.lengthにアクセスできる return p.length; } // stringが渡せる console.log(countParameter("This is a test string code.")); // => 27 // Arrayが渡せる console.log(countParameter(["1", "2", "3"])); // => 3 // numberは渡せない // TS2345: Argument of type '1' is not assignable to parameter of type 'Lengthy'. console.log(countParameter(1)); |
lengthプロパティを持つinterfaceをcountParameterのTに実装しました。
このようにすると、関数の呼び出し元では、lengthプロパティを元々持っているstring型やArray型を引数として渡すことができます。
逆にlengthプロパティを持っていないnumber型やboolean型などを渡した場合は、エラーになります。
このようにジェネリック型を使って曖昧にしておくと、柔軟に呼び出すことが可能です。
keyofで制約をつける
keyofを使うとオブジェクトが特定のプロパティを持っていることを保証できるようにできます。
例えば、以下のオブジェクトがあるとします。
1 2 3 |
const book = { title: "Harry Potter" } |
この場合は、bookオブジェクトがtitleを持っていることを保証したいです。
そこで、keyofを使って制約をかけます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// keyof TでTのプロパティであることを宣言する function returnBookTitle<T extends object, U extends keyof T>(obj: T, key: U) { return `This book title is ${obj[key]}`; } const book = { title: "Harry Potter", }; console.log(returnBookTitle(book, "title")); // bookにtitleが含まれてるからOK // error // TS2345: Argument of type '"hoge"' is not assignable to parameter of type '"title"'. console.log(returnBookTitle(book, "hoge")); |
これで、Bookにtitleが存在する制約をつけることが可能になりました。これは、結構便利なんです。
ジェネリッククラス
ジェネリックは、クラスにも使うことが可能です。
例えば、以下のようなクラスがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Archive { private data = []; addData(item) { this.data.push(item); } removeData(item) { this.data.splice(this.data.indexOf(item), 1); } getData() { return [...this.data]; } } |
このクラスは、なんらかのデータを保存したり、削除したり、取得したりすることができます。
dataやitemには型指定を指定ないので、現時点ではエラーが表示されます。
1 2 3 |
# error (parameter) item: any Parameter 'item' implicitly has an 'any' type. |
通常なら型を指定する必要がありますが、ジェネリック型を使うことで、柔軟なデータを渡せることができるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Archive<T> { private data: T[] = []; addData(item: T) { this.data.push(item); } removeData(item: T) { this.data.splice(this.data.indexOf(item), 1); } getData() { return [...this.data]; } } |
上記では、T型(文字列はT出なくても良い)を指定しました。これで、number型、string型、boolean型など型のタイプを気にせず、コードを呼び出せるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// string型を指定する const textArchive = new Archive<string>(); textArchive.addData("test"); console.log(textArchive.getData()); textArchive.removeData("test"); // number型を指定する const numberArchive = new Archive<number>(); numberArchive.addData(1); console.log(numberArchive.getData()); numberArchive.removeData(1); // Union型もOK const mixArchive = new Archive<string | number>(); mixArchive.addData("test"); mixArchive.addData(1); console.log(mixArchive.getData()); |
便利そうですよね。
ちなみにクラスに対しても制約をつけることが可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Archive<T extends string | number | boolean> { private data: T[] = []; addData(item: T) { this.data.push(item); } removeData(item: T) { this.data.splice(this.data.indexOf(item), 1); } getData() { return [...this.data]; } } |
おわりに
ジェネリック型については、以上になります。
ジェネリック型を使用することで、呼び出し元で型を自由に決定つけることができるので、大変便利です。
実際のプロジェクトでもガンガン使っていければと思います^^
それでは、また!
最近のコメント