ジェネリクスの役立つ主な理由は、メンバ間で意味のある型制約を提供することです。メンバには以下のものがあります:
- クラスのインスタンスメンバ
- クラスメソッド
- 関数の引数
- 関数の戻り値
単純なQueue
(先入れ先出し)データ構造の実装を考えてみましょう。TypeScript/JavaScriptの単純なものは以下のようになります:
class Queue {
private data = [];
push(item) { this.data.push(item); }
pop() { return this.data.shift(); }
}
この実装での1つの問題は、キューに何でも追加できることです。また、キューから要素を取り出すと、何が出てくるかわかりません。これを以下に示します。ここでは誰かがstring
をキューにプッシュしていますが、実際にはnumbers
だけがプッシュされることを想定しています。
class Queue {
private data = [];
push(item) { this.data.push(item); }
pop() { return this.data.shift(); }
}
const queue = new Queue();
queue.push(0);
queue.push("1"); // Oops a mistake
// a developer walks into a bar
console.log(queue.pop().toPrecision(1));
console.log(queue.pop().toPrecision(1)); // RUNTIME ERROR
1つの解決策(実際にはジェネリクスをサポートしていない言語での唯一の解決策)は、これらの制約のために特別なクラスを作成することです。例えば素早くダーティに数値型のキューを作ります:
class QueueNumber extends Queue {
push(item: number) { super.push(item); }
pop(): number { return this.data.shift(); }
}
const queue = new QueueNumber();
queue.push(0);
queue.push("1"); // ERROR : cannot push a string. Only numbers allowed
// ^ if that error is fixed the rest would be fine too
もちろん、これはすぐに苦痛になる可能性があります。文字列キューが必要な場合は、そのすべての作業をもう一度行う必要があります。あなたが本当に必要とすることは、型が何であれ、プッシュされているものの型とポップされたものの型は同じでなければならないということです。これは、ジェネリクスパラメータ(この場合はクラスレベル)で簡単に行えます:
/** A class definition with a generic parameter */
class Queue<T> {
private data = [];
push(item: T) { this.data.push(item); }
pop(): T | undefined { return this.data.shift(); }
}
/** Again sample usage */
const queue = new Queue<number>();
queue.push(0);
queue.push("1"); // ERROR : cannot push a string. Only numbers allowed
// ^ if that error is fixed the rest would be fine too
すでに見たもう1つの例は、_reverse_関数の例です。ここでは、関数に渡されるものと関数が返すものの間の制約があります。
function reverse<T>(items: T[]): T[] {
var toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1
// Safety!
reversed[0] = '1'; // Error!
reversed = ['1', '2']; // Error!
reversed[0] = 1; // Okay
reversed = [1, 2]; // Okay
このセクションでは、クラスレベルと関数レベルで定義されているジェネリクスの例を見てきました。多少付け加えたいことは、メンバ関数のためだけにジェネリクスを作成できるということです。おもちゃの例として、reverse
関数をUtility
クラスに移したところで、次のことを考えてみましょう:
class Utility {
reverse<T>(items: T[]): T[] {
var toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
}
ヒント:必要に応じてジェネリクスパラメータを呼び出すことができます。単純なジェネリックを使うときは
T
、U
、V
を使うのが普通です。複数のジェネリクス引数がある場合は、意味のある名前を使用してください。例えばTKey
とTValue
です(一般にT
を接頭辞として使用する規約は、他の言語(例えばC++)では_テンプレート_と呼ばれることもあります)。
次の関数を考えてみましょう。
declare function parse<T>(name: string): T;
この場合、タイプT
は1つの場所でのみ使用されていることがわかります。したがって、メンバ間に制約はありません。これは、型安全性における型アサーションに相当します。
declare function parse(name: string): any;
const something = parse('something') as TypeOfSomething;
1回だけ使用されるジェネリクスは、型安全性に関してはアサーションよりも劣っています。それは、既に述べたように、あなたのAPIに利便性を提供するものです。
より明らかな例は、jsonレスポンスをロードする関数です。それは、あなたが渡した任意の型のPromiseを返します:
const getJSON = <T>(config: {
url: string,
headers?: { [key: string]: string },
}): Promise<T> => {
const fetchConfig = ({
method: 'GET',
'Accept': 'application/json',
'Content-Type': 'application/json',
...(config.headers || {})
});
return fetch(config.url, fetchConfig)
.then<T>(response => response.json());
}
あなたは依然としてアノテーションを明示しなければならないことに注意してください。しかし、getJSON<T>
のシグネチャ(config) => Promise<T>
は、キータイプを減らすことができます(loadUsers
の戻り値の型は、TypeScriptが推論可能なのでアノテーションする必要はありません):
type LoadUsersResponse = {
users: {
name: string;
email: string;
}[]; // array of user objects
}
function loadUsers() {
return getJSON<LoadUsersResponse>({ url: 'https://example.com/users' });
}
戻り値としてのPromise<T>
は、Promise<any>
よりも断然優れています。
次の例では、ジェネリクスが引数のみに使用されています。
declare function send<T>(arg: T): void;
このようにすると、次のように、引数にマッチさせたい型をアノテートするのにジェネリクスT
を利用できます。
send<Something>({
x:123,
// Also you get autocomplete
}); // Will TSError if `x:123` does not match the structure expected for Something