動作環境
今回もPlaygroundでガシガシ書いていきます。
TypeScript Playground
参考
ジェネリクスとは
汎用的なメソッドやクラスに対して、様々な型を紐付けることができる機能です。
通常TypeScriptでは以下のように関数を定義します。
function returnValue(value: string): string {
return value;
}
ですが、これだと引数にString型しか渡せません。Number型やArray型など他の型でも使いたいとなったら、同じような関数を何個も定義することになってしまいます。
ここで、ジェネリクスの出番です。先程のreturnValue
をジェネリクスを使って書き換えてみます。
function returnValue<T>(value: T): T {
return value;
}
let result = returnValue<number>(1);
console.log(result); // => 1
result = returnValue<string>('Hello, Generic!!');
console.log(result); // => Hello, Generic!!
関数名の後<T>
を追加が追加されています。このT
は型引数と言いい、型を引数のように呼び出し時に渡すことができます。
今回の例だと、型引数が引数value
と戻り値のデータ型にもなっているので、
引数として受け取った型T
のvalueを受け取り、戻り値の型もT
になるという意味です。
呼び出すときには、returnValue<string>('Hello, Generic!!');
といったように、<>
でデータ型を囲って指定します。
※ 慣例的に、型引数にはT
, U
, R
などがよく使われるようです。
ジェネリクスと関数
もう少し複雑な例をあげようと思います。
以下のような、id
プロパティを持つPost
クラスとCategory
クラスがあるとします。
class Post {
id: number;
title: string;
constructor(id: number, title: string) {
this.id = id;
this.title = title;
}
}
class Category {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
この2つのクラスの配列に対して同一のid
のものがあれば、要素を置き換えるようなreplace
関数を実装してみます。
replace(array, newItem);
(Reduxのreducerで良く行いそうな処理ですね。)
ひとまず、実装してみる
function replace<T>(data: T[], newItem: T): T[] {
const index = data.findIndex(item => item.id === newItem.id); // Property 'id' does not exist on type 'T'.
if (index < 0) {
console.error(`id: ${newItem.id} not found.`); // Property 'id' does not exist on type 'T'.
return data;
}
return [
...data.slice(0, index),
newItem,
...data.slice(index + 1),
];
}
完成!!簡単!と言いたいところですが、エラーが出てしまっています。
Property 'id' does not exist on type 'T'.
(Tには、id
プロパティが無いよー)
そこで、「T
にはid
プロパティがある」という制約を付けます。
型引数T
に制約をつける
extends
キーワードを使うことで制約をつけれます。
今回の例では、<T>
を<T extends { id: number }
とするれば、「T
は { id: number}
とい構造を持っている(継承している)」という制約を付けることができます。
その他にも、例えば、<T extends MyClass>
とクラス名を書くことで、型引数T
には、「MyClass
とその派生クラスだけを受け取る」という制約をつけることができます。
それでは、先程のコードを書き換えましょう。
function replace<T extends { id: number }>(data: T[], newItem: T): T[] {
const index = data.findIndex(item => item.id === newItem.id);
if (index < 0) {
console.error(`id: ${newItem.id} not found.`);
return data;
}
return [
...data.slice(0, index),
newItem,
...data.slice(index + 1),
];
}
これで完成です。実際に呼び出して確認してみます。
動作確認
const postArray = [
new Post(1, 'post1'),
new Post(2, 'post2'),
new Post(3, 'post3'),
new Post(4, 'post4')
];
const categoryArray = [
new Category(1, 'typescript'),
new Category(2, 'coffeescript'),
new Category(3, 'es6'),
]
const newPost = new Post(3, 'TypeScriptについて');
const newCategory = new Category(1, 'TypeScript');
let postArrayResult = replace(postArray, newPost);
console.log(postArrayResult);
let categoryArrayResult = replace(categoryArray, newCategory);
console.log(categoryArrayResult);

ジェネリクスとクラス
クラス名の後に<T>
のような型引数を付与すれば、クラスに対しても使用することができます。
以下は、TypeScript Handbookの例に少し手を加えたものです。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
getZeroValue(): T {
return this.zeroValue;
}
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };
console.log(myGenericNumber.getZeroValue()); // => 0
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); // => test
クラスに付与された型引数は、そのクラスのプロパティやメソッド定義で使用することができます。
ジェネリクスまとめ
- 関数名やクラス名の後に
<T>
といったように型引数を付与する - 型引数は関数内やクラス内で使用することができる
- 型引数に制約を付けたい場合は
<T extends ParentClass>
のようにextends
を使う