Generics

ジェネリック(Generics)

ジェネリックの役立つ主な理由は、メンバ間で意味のある型制約を提供することです。メンバには以下のものがあります:

  • クラスのインスタンスメンバ

  • クラスメソッド

  • 関数の引数

  • 関数の戻り値

ジェネリックのモチベーションとサンプル

単純なQueue(先入れ先出し)データ構造の実装を考えてみましょう。TypeScript/JavaScriptの単純なものは以下のようになります:

class Queue {
private data = [];
push = (item) => this.data.push(item);
pop = () => this.data.shift();
}

この実装での1つの問題は、キューに何でも追加できることです。また、キューから要素を取り出すと、何が出てくるかわかりません。これを以下に示します。ここでは誰かがstringをキューにプッシュしていますが、実際にはnumbersだけがプッシュされることを想定しています。

class Queue {
private data = [];
push = (item) => this.data.push(item);
pop = () => 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 {
private data = [];
push = (item: number) => this.data.push(item);
pop = (): number => 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 => 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

すでに見たもう一つの例は、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;
}
}

ヒント:必要に応じてジェネリックパラメータを呼び出すことができます。単純なジェネリックを使うときは TUVを使うのが普通です。複数のジェネリック引数がある場合は、意味のある名前を使用してください。例えばTKeyTValueです(一般にTを接頭辞として使用する規約は、他の言語(例えばC++)ではテンプレートと呼ばれることもあります)。

役に立たずのジェネリック

私は人々が興味本位でジェネリックを使用しているのを見ました。考えるべき質問は、何を表現しようとしているのか?です。あなたが簡単にそれに答えることができない場合は、役に立たないジェネリックかもしれません。例えば次の関数です:

declare function foo<T>(arg: T): void;

ここでは、ジェネリックTは、一箇所の引数の位置でのみ使用されるので完全に不要です。これは次のコードと同じです:

declare function foo(arg: any): void;

デザインパターン:便利なジェネリック

次の関数を考えてみましょう。

declare function parse<T>(name: string): T;

この場合、タイプTは1つの場所でのみ使用されていることがわかります。したがって、メンバー間に制約はありません。これは、型安全性における型アサーションに相当します。

declare function parse(name: string): any;
const something = parse('something') as TypeOfSomething;

一回だけ使用されるジェネリックは、型安全性に関してはアサーションよりも劣っています。それは、既に述べたように、あなたの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>よりも断然優れています。