一.存在意義
このようなシナリオを考えます。identity 関数は 1 つのパラメータを受け取り、そのまま返します:
function identity(arg) {
return arg;
}
タイプから見ると、パラメータがどのようなタイプであっても、戻り値のタイプはパラメータと一致します。[オーバーロードメカニズム](/articles/関数-typescript ノー��� 5/#articleHeader9) を借助して、このように記述できます:
function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...等无数个 a => a のタイプ記述
オーバーロードはこのシナリオを満足できないようです。arg のすべての可能なタイプを列挙する方法がないからです。パラメータが任意のタイプなので、any を試してみましょう:
function identity(arg: any): any;
すべてのタイプをカバーしましたが、パラメータと戻り値のタイプ対応関係を失いました(上はA => Bのタイプマッピングに相当し、私たちが記述したいのはA => Aです)
ジェネリックと any
では、2 つの any 間の対応関係をどのように表現すればよいでしょうか?
ジェネリックを使用します。このように記述:
function identity<T>(arg: T): T {
return arg;
}
タイプ変数 T は any に類似し、具名 any に相当 します。これで T => T(つまり A => A)のタイプマッピングを明確に表現できます
二.タイプ変数
Type variable, a special kind of variable that works on types rather than values.
普通の変数は値を表し、タイプ変数はタイプを表します
作用から見ると、変数は値を運搬でき、タイプ変数はタイプ情報を運搬します:
This allows us to traffic that type information in one side of the function and out the other.
三.ジェネリック関数
タイプ変数はタイプパラメータとも呼ばれ、関数パラメータに類似しています。違いは関数パラメータが具体的な値を受け取り、タイプパラメータが具体的なタイプを受け取ることです。例えば:
function identity<T>(arg: T): T {
return arg;
}
// 传参给类型参数
// identity<number>
// 传参给函数参数(自动推断类型参数)
identity(1);
// 传参给函数参数(显式传入类型参数)
identity<number>(1);
タイプパラメータを持つ関数をジェネリック関数と呼び、その中でタイプパラメータは任意のタイプ(any and all types)を表します。そのため、すべてのタイプに共通する特徴のみアクセスできます:
function loggingIdentity<T>(arg: T): T {
// 报错 Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}
実際、void という空集合があるため、すべてのタイプに共通するプロパティやメソッドは存在しません。タイプ変数に対して仮定をすることもできません(例えば length プロパティがあると仮定)。任意のタイプを表し、何の制約もない ためです
除此之外、タイプ変数 T は具体的なタイプのように、具体的なタイプが現れるあらゆる場所で使用できます:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
// または
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
タイプ記述
ジェネリック関数のタイプ記述は普通関数に類似:
// 普通関数
let myIdentity: (arg: string) => string =
function(arg: string): string {
return arg;
};
// ジェネリック関数
let myIdentity: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
依然としてアロー関数構文ですが、(パラメータリスト) の前に <タイプパラメータリスト> を追加しただけです。同様に、タイプ記述中のタイプパラメータ名も実際のものと一致しなくてもよい:
let myIdentity: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
P.S. 特殊的、関数タイプ記述はオブジェクトリテラル形式でも記述可能:
// ジェネリック関数
let myIdentity: { <T>(arg: T): T };
// 普通関数
let myIdentity: { (arg: string): string };
インターフェース形式タイプ記述の退化バージョンのようで、再利用の優位性がなく、アロー関数ほど簡潔でもないため、あまり一般的ではありません
四.ジェネリックインターフェース
タイプパラメータを持つインターフェースをジェネリックインターフェースと呼びます。例えばインターフェースを使用してジェネリック関数を記述:
interface GenericIdentityFn {
<T>(arg: T): T;
}
非常に似た形式がもう 1 つあります:
interface GenericIdentityFn<T> {
(arg: T): T;
}
これらはどちらもジェネリックインターフェースと呼ばれます。違いは後者のタイプパラメータ T がインターフェース全体に作用することです。例えば:
interface GenericIdentity<T> {
id(arg: T): T;
idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
id: (s: string) => s,
// 报错 Types of parameters 's' and 'args' are incompatible.
idArray: (...s: number[]) => s,
};
インターフェースレベルのタイプパラメータにはこのような拘束作用がありますが、メンバーレベルのものはありません(該ジェネリックメンバーのみに作用)
五.ジェネリッククラス
同様に、タイプパラメータを持つクラスをジェネリッククラスと呼びます。例えば:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
インターフェースのように、ジェネリッククラスは該クラスのすべてのメンバーが注目する目標タイプが一致することを拘束できます:
Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
注意、タイプパラメータはクラス中のインスタンスメンバーのみに適用され、静的メンバーはタイプパラメータを使用できません。例えば:
class GenericNumber<T> {
// 报错 Static members cannot reference class type parameters.
static zeroValue: T;
}
静的メンバーはクラスインスタンス間で共有され、タイプパラメータの具体的なタイプを一意に確定できないため:
let n1: GenericNumber<string>;
// 期望 n1.constructor.zeroValue は string
let n2: GenericNumber<number>;
// 期望 n1.constructor.zeroValue は number、矛盾が発生
P.S. この点は Java と一致、詳細は Static method in a generic class? 参照
六.ジェネリック拘束
タイプパラメータはあまりに「汎」すぎ(any and all)、いくつかのシナリオでは、拘束を加えたい場合があります。例えば:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
インターフェースを通じてタイプパラメータへの拘束を記述(T extends constraintInterface)、例えば上はタイプパラメータ T ��� number タイプの length プロパティを持たなければならないと要求
もう 1 つの典型的なシナリオはファクトリメソッド、例えば:
// 要求コンストラクタ c は必ず同一クラス(またはサブクラス)のインスタンスを返す
function create<T>(c: {new(): T; }): T {
return new c();
}
此外、ジェネリック拘束中でタイプパラメータを使用することも可能、例えば:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
1 つのタイプパラメータの特徴を使用して別のタイプパラメータを拘束でき、非常に強力です
七.まとめ
ジェネリックと呼ばれるのは、一連のタイプに作用できるためで、具体的なタイプの上の 1 層の抽象です:
Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
参考資料
-
[タイプパラメータ | タイプ_Haskell ノート 3](/articles/タイプ-haskell ノート 3/#articleHeader9)
コメントはまだありません