メインコンテンツへ移動

ジェネリック_TypeScript ノート 6

無料2019-02-09#TypeScript#TypeScript generic#TypeScript generic function#TS泛型函数#TS泛型类#TS泛型接口

ジェネリックと呼ばれるのは、一連の型に作用できるためで、具体的な型の上の 1 層の抽象です

一.存在意義

このようなシナリオを考えます。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;
}

タイプ変数 Tany に類似し、具名 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.

参考資料

  • Generics

  • [タイプパラメータ | タイプ_Haskell ノート 3](/articles/タイプ-haskell ノート 3/#articleHeader9)

コメント

コメントはまだありません

コメントを書く