TypeScript ジェネリックス

導入

場合によっては、関数の戻り値の型がパラメーターの型に関連していることがあります。

関数 getFirst(arr) {
  arr[0]を返します;
}

上の例では、関数 getFirst() は常にパラメーター配列の最初のメンバーを返します。パラメータ配列の型と戻り値の型。

この関数の型宣言は以下のようにしか書けません。

関数 f(arr:any[]):any {
  arr[0]を返します;
}

上記の型宣言は、パラメーターと戻り値の間の型の関係を反映していません。

この問題を解決するために、TypeScript は「ジェネリック」を導入しました。ジェネリックの特徴は「型パラメータ」を持つことです。

関数 getFirst<T>(arr:T[]):T {
  arr[0]を返します;
}

上記の例では、関数 getFirst() の関数名の後の山括弧 <T> の部分が型パラメータであり、パラメータは一対の山括弧 (<>) 内に配置する必要があります。この例には、型パラメータ T が 1 つだけあります。これは型宣言に必要な変数として理解でき、呼び出し時に特定のパラメータ型を渡す必要があります。

上の例の関数 getFirst() のパラメータの型は T[]、戻り値の型は T であり、両者の関係が明確に示されています。たとえば、入力パラメータの型が number[] の場合、T の値は number であるため、戻り値の型も number になります。

関数を呼び出すときは、型パラメータを指定する必要があります。

getFirst<数値>([1, 2, 3])

上の例では、関数 getFirst() を呼び出すときに、関数名の後に山括弧を使用して型パラメータ T の値 (この場合は <number>) を指定する必要があります。

ただし、便宜上、関数を呼び出すとき、type パラメーターの値は省略され、TypeScript によって推論されることが許可されることがよくあります。

getFirst([1, 2, 3])

上記の例では、TypeScript は実際のパラメーター [1, 2, 3] から型パラメーター T の値が number であると推測します。

一部の複雑な使用シナリオでは、TypeScript が type パラメーターの値を推測できない場合があり、この場合は明示的に指定する必要があります。

関数 comb<T>(arr1:T[], arr2:T[]):T[] {
  戻り arr1.concat(arr2);
}

上記の例では、2 つのパラメータ arr1arr2 と戻り値は同じ型です。 type パラメータの値が指定されていない場合、次の呼び出しではエラーが報告されます。

comb([1, 2], ['a', 'b']) // エラー

上記の例では、TypeScript は 2 つのパラメーターが同じ型ではないと判断してエラーを報告します。ただし、型パラメータが共用体型の場合、エラーは報告されません。

comb<number|string>([1, 2], ['a', 'b']) // 正しい

上記の例では、型パラメータは共用体型であるため、両方のパラメータが型パラメータに準拠しており、エラーは報告されません。この場合、型パラメータは省略できません。

型パラメータの名前は任意に選択できますが、正当な識別子である必要があります。慣例により、型パラメータの最初の文字は常に大文字です。通常、「T」(型の最初の文字)が型パラメータの名前として使用されます。複数の型パラメータがある場合は、T の後に U や V などの文字を使用して名前を付け、カンマ (「,」) を使用して各パラメータを区切ります。

以下は複数の型パラメータの例です。

関数マップ<T, U>(
  arr:T[]、
  f:(引数:T) => U
):U[] {
  arr.map(f)を返します;
}

// 使用例
マップ<文字列、数値>(
  ['1', '2', '3'],
  (n) => parseInt(n)
); // [1、2、3] を返します。

上記の例では、配列のインスタンス メソッド map() を、2 つの型パラメータ TU を持つグローバル関数に書き換えます。意味は、元の配列の型が T[] で、配列の各メンバーに対して処理関数 f を実行し、型 T を型 U に変換すると、型が得られるということです。 U []` 配列。

つまり、ジェネリックは、表現するために型パラメーターを必要とする型ロジックの一部として理解できます。型パラメーターを使用すると、入力型と出力型の間に 1 対 1 の対応を確立できます。

ジェネリックスの書き方

ジェネリックは主に、関数、インターフェイス、クラス、エイリアスの 4 つの状況で使用されます。

関数の一般的な記述

前のセクションで説明したように、「function」キーワードで定義された汎用関数の場合、型パラメータは山括弧で囲まれ、関数名の後に記述されます。

関数 id<T>(arg:T):T {
  引数を返します。
}

したがって、変数形式で定義された関数の場合、ジェネリックスには次の 2 つの書き方があります。

// 書き方その1
let myId:<T>(arg:T) => T = id;

//書き方2
let myId:{ <T>(arg:T): T } = id;

インターフェースの一般的な書き方

インターフェイスは汎用形式で記述することもできます。

インターフェース Box<Type> {
  内容: タイプ;
}

let ボックス:Box<文字列>;

上記の例では、ジェネリック インターフェイスを使用する場合、type パラメーターの値を指定する必要があります (この場合は string)。

別の例を示します。

インターフェイス Comparator<T> {
  比較対象(値:T): 数値;
}

class Rectangle は Comparator<Rectangle> {を実装します。

  CompareTo(値:Rectangle): 数値 {
    // ...
  }
}

上記の例では、汎用インターフェイスが最初に定義され、次にクラスで使用されます。

汎用インターフェイスを作成する 2 番目の方法があります。

インターフェイス Fn {
  <タイプ>(引数:タイプ): タイプ;
}

関数 ID<タイプ>(引数:タイプ): タイプ {
  引数を返します。
}

myId:Fn = id; にします。

上記の例では、Fn の型パラメータ Type の特定の型は、使用時に関数 id によって提供される必要があります。したがって、最後の行の代入ステートメントでは、特定のタイプの Type を指定する必要はありません。

また、2番目の書き方にはもう一つ違いがあります。つまり、その型パラメーターは特定のメソッドで定義されており、他のプロパティやメソッドはこの型パラメーターを使用できません。最初の記述方法では、型パラメータがインターフェイス全体で定義され、インターフェイス内のすべてのプロパティとメソッドがこの型パラメータを使用できます。

クラスの一般的な記述

ジェネリック クラスの型パラメータはクラス名の後に記述されます。

クラス ペア<K, V> {
  キー: K;
  値: V;
}

以下はジェネリッククラスからの継承の例です。

クラス A<T> {
  値: T;
}

class B extends A<any> {
}

上記の例では、クラス A には型パラメータ T があり、それを使用するときに T の型を指定する必要があるため、クラス B が継承する場合は A<any> として記述する必要があります。 。

ジェネリックはクラス式でも使用できます。

const コンテナ = class<T> {
  コンストラクター(プライベート読み取り専用データ:T) {}
};

const a = 新しいコンテナ<boolean>(true);
const b = 新しいコンテナ<番号>(0);

上記の例では、新しいインスタンスを作成する際に、型パラメータTとクラスパラメータdataの値を同時に与える必要があります。

別の例を示します。

クラス C<NumType> {
  値!: NumType;
  add!: (x: NumType, y: NumType) => NumType;
}

let foo = new C<number>();

foo.value = 0;
foo.add = 関数 (x, y) {
  x + y を返します。
};

上記の例では、まずクラス C の新しいインスタンス foo を作成し、次にそのインスタンスの value 属性と add() メソッドを定義します。クラスの定義では、プロパティとメソッドの後の感嘆符は非 null アサーションであり、それらが非 null であり、後で値が割り当てられることを TypeScript に伝えます。

JavaScript クラスは本質的にコンストラクターであるため、ジェネリック クラスもコンストラクターとして作成できます。

type MyClass<T> = new (...args: any[]) => T;

// または
インターフェース MyClass<T> {
  new(...引数: 任意[]): T;
}

// 使用例
関数 createInstance<T>(
  AnyClass: MyClass<T>、
  ...引数: 任意[]
):T {
  新しい AnyClass(...args) を返します。
}

上記の例では、関数 createInstance() の最初のパラメータ AnyClass はコンストラクター (クラスにすることもできます) であり、その型は MyClass<T> です。ここで、TcreateInstance( ) type パラメータは、関数が呼び出されるときに特定の型を指定します。

ジェネリック クラスは、静的プロパティと静的メソッドを除いて、クラス自体で定義されているため、クラスのインスタンスを記述することに注意してください。したがって、型パラメータを参照することはできません。

クラス C<T> {
  静的データ: T; // エラーレポート
  コンストラクター(公開値:T) {}
}

上記の例では、静的属性 data は型パラメータ T を参照していますが、型パラメータはインスタンス プロパティとインスタンス メソッドにのみ使用できるため、これは許可されず、エラーが報告されます。

型エイリアスの一般的な記述

type コマンドで定義された型エイリアス、ジェネリックも使用できます。

型 Nullable<T> = T 未定義 |

上の例では、Nullable<T> はジェネリック型です。型を渡す限り、この型の unknownnull の共用体型を取得できます。

別の例を示します。

type Container<T> = { 値: T };

const a: Container<number> = { 値: 0 };
const b: Container<string> = { value: 'b' };

以下にツリー構造を定義する例を示します。

type Tree<T> = {
  値: T;
  左: Tree<T> | null;
  右: Tree<T> | null;
};

上記の例では、型エイリアス Tree は内部的に Tree 自体を再帰的に参照します。

typeパラメータのデフォルト値

型パラメータにはデフォルト値を設定できます。使用する場合、type パラメーターに値が指定されていない場合は、デフォルト値が使用されます。

関数 getFirst<T = string>(
  編:T[]
):T {
  arr[0]を返します;
}

上記の例で、「T = string」は、型パラメータのデフォルト値が「string」であることを意味します。 getFirst() を呼び出すときに、T の値が指定されていない場合、TypeScript は Tstring に等しいと想定します。

ただし、TypeScript は実際のパラメーターから「T」の値を推測し、デフォルト値をオーバーライドするため、次のコードはエラーを報告しません。

getFirst([1, 2, 3]) // 正しい

上記の例では、実際のパラメータは [1, 2, 3] であり、TypeScript は T が number に等しいと推論し、デフォルト値 string をオーバーライドします。

型パラメータのデフォルト値はクラスでよく使用されます。

class Generic<T = string> {
  リスト:T[] = []

  add(t:T) {
    this.list.push(t)
  }
}

上記の例では、クラス Generic には型パラメータ T があり、デフォルト値は string です。これは、属性 list がデフォルトで文字列の配列であり、メソッド add() のデフォルトのパラメータが文字列であることを意味します。

const g = 新しいジェネリック();

g.add(4) // エラーを報告する
g.add('hello') // 正しい

上の例では、「Generic」の新しいインスタンス「g」を作成するときに、型パラメータ「T」の値が指定されていないため、「T」は「string」と等しくなります。したがって、「add()」メソッドに値を渡すとエラーが発生しますが、文字列を渡すとエラーは発生しません。

const g = new Generic<number>();

g.add(4) // 正しい
g.add('hello') // エラーレポート

上の例では、新しいインスタンス g を作成するときに、型パラメータ T の値が number として指定されるため、add() メソッドは値を渡すときにエラーを報告しませんが、文字列を渡すとエラーが報告されます。

型パラメータにデフォルト値が設定されると、それはオプションであることを意味します。複数の型パラメータがある場合は、必須パラメータの後にオプションのパラメータを指定する必要があります。

<T = boolean, U> // エラー

<T, U = boolean> // 正しい

上の例では、2 つの型パラメータ TU があります。 「T」がオプションのパラメータで、「U」がそうでない場合、エラーが報告されます。

配列の一般的な表現

「配列」の章で述べたように、配列型の表現方法の 1 つは Array<T> です。これがジェネリックの記述方法です。「Array」は TypeScript 固有の型インターフェイスであり、「T」はその型パラメータです。配列を宣言するときは、「T」の値を指定する必要があります。

arr:Array<数値> = [1, 2, 3]; とします。

上の例では、Array<number> はジェネリック型で、型パラメーターの値は number です。これは、配列のすべてのメンバーが数値であることを意味します。

同様に、配列メンバーがすべて文字列の場合、型は Array<string> として記述されます。実際、TypeScript 内で配列型を記述する他の方法である number[]string[] は、単に Array<number>Array<string> の省略形にすぎません。

TypeScript 内では、Array は汎用インターフェイスであり、型定義は基本的に次のようになります。

インターフェース Array<Type> {

  長さ: 数値;

  Pop(): タイプ|未定義;

  Push(...items:Type[]): 数値;

  // ...
}

上記のコードでは、push() メソッドのパラメータ item の型は Type[] であり、これは Array() のパラメータ型 Type と一致しています。つまり、同タイプの追加も可能です。 push() を呼び出すと、TypeScript は 2 つが一貫しているかどうかをチェックします。

MapSetPromise などの他の TypeScript 内部データ構造は、実際には汎用インターフェイスであり、完全な書き込みメソッドは Map<K, V>Set<T> および Promise<T> です。 >

TypeScript は、読み取り専用配列を表す ReadonlyArray<T> インターフェイスもデフォルトで提供します。

関数 doStuff(
  値:読み取り専用配列<文字列>
) {
  value.push('hello!'); // エラーレポート
}

上の例では、パラメーター values のタイプは ReadonlyArray<string> です。これは、配列を変更できないことを意味するため、関数本体内に新しい配列メンバーを追加するとエラーが発生します。したがって、関数内でパラメーター配列を変更したくない場合は、パラメーター配列を型 ReadonlyArray<T> として宣言できます。

型パラメータの制約

多くの型パラメーターは無制限ではなく、渡される型には制約があります。

function comp<Type>(a:Type, b:Type) {
  if (a.length >= b.length) {
    を返します。
  }
  bを返します。
}

上の例では、型パラメータ Type に隠れた制約があります。つまり、「length」プロパティが必要です。この条件が満たされない場合、エラーが報告されます。

TypeScript は、型パラメータに制約を記述できる構文を提供します。条件が満たされない場合、コンパイル時にエラーが報告されます。これは、適切なセマンティクスを持ち、型パラメータを説明することもできます。

関数 comp<T extends { 長さ: 数値 }>(
  で、
  b:T
) {
  if (a.length >= b.length) {
    を返します。
  }
  bを返します。
}

上の例では、「T extends { length:number }」は制約です。これは、型パラメータ T が { length: number } を満たさなければならないことを意味します。そうでない場合は、エラーが報告されます。

comp([1, 2], [1, 2, 3]) // 正しい
comp('ab', 'abc') // 正しい
comp(1, 2) // エラーを報告する

上記の例では、渡されたパラメーターの型が制約を満たさない限り、エラーが報告されます。

型パラメータの制約は次の形式になります。

<TypeParameter は ConstraintType を拡張します>

上記の構文では、TypeParameter は型パラメータを表し、extends は必須のキーワードであり、ConstraintType は型パラメータが満たさなければならない条件を表します。つまり、型パラメータは のサブタイプである必要があります。制約タイプ

型パラメータは、デフォルト値が制約を満たす必要がある場合に限り、制約とデフォルト値を同時に設定できます。

type Fn<A は文字列を拡張、B は文字列を拡張 = 'world'>
  = [A, B];

type Result = Fn<'hello'> // ["hello", "world"]

上の例では、型パラメータ AB の両方に制約があり、B にもデフォルト値があります。したがって、「Fn」を呼び出すときは、「A」の値のみを指定でき、「B」の値は指定できません。

さらに、上記の例から、ジェネリックは本質的にパラメーターを入力して結果を取得する型関数であることもわかります。2 つは 1 対 1 に対応しています。

複数の型パラメーターがある場合、1 つの型パラメーターに対する制約が他のパラメーターを参照できます。

<T、U は T を拡張します>
// または
<T は U, U を拡張>

上記の例では、「U」の制約が「T」を参照するか、「T」の制約が「U」を参照するのが正しいです。

ただし、制約は型パラメータ自体を参照できません。

<T extends T> // エラーを報告する
<T extends U, U extends T> // エラーを報告する

上の例では、「T」の制約を「T」そのものにすることはできません。同様に、複数の型パラメータは互いに制約することはできません (つまり、T の制約条件は U であり、U の制約条件は T です)。これは、相互制約とは、制約条件が次のとおりであることを意味するためです。型パラメータそのものです。

使用上の注意

ジェネリック医薬品を使用する場合には、いくつかの注意点があります。

**(1) ジェネリック医薬品はできるだけ使用しないでください。 **

ジェネリックは柔軟性がありますが、コードが複雑になり、読み書きが困難になります。一般に、ジェネリックスが使用される場合、型宣言は通常読みにくく、複雑になりやすいです。したがって、ジェネリックを使用したくない場合は、使用しないでください。

**(2) 型パラメータは少ないほど良いです。 **

型パラメーターと置換手順が 1 つ増えると、複雑さが増します。したがって、型パラメータは少ないほど良いのです。

関数フィルター<
  Tさん
  Fn extends (arg:T) => ブール値
>(
  arr:T[]、
  機能:Fn
): T[] {
  戻り arr.filter(func);
}

上記の例には 2 つの型パラメータがありますが、2 番目の型パラメータ Fn は不要であり、関数パラメータの型宣言に直接記述することができます。

関数フィルター<T>(
  arr:T[]、
  func:(arg:T) => ブール値
): T[] {
  戻り arr.filter(func);
}

上記の例では、type パラメーターは 1 に簡略化されており、効果は前の例と同じです。

**(3) 型パラメータは 2 回出現する必要があります。 **

型パラメータが定義後に 1 回だけ出現する場合は、おそらく不要です。

functiongreet<Str extends string>(
  s:Str
) {
  console.log('こんにちは、' + s);
}

上記の例では、型パラメータ Str は関数宣言内 (定義部分を除く) に 1 回だけ出現します。これは、多くの場合、この型パラメータが不要であることを示しています。

関数greet(s:string) {
  console.log('こんにちは、' + s);
}

上の例では前の型パラメータが省略されており、効果は前の例と同じです。

つまり、型パラメーターが 2 回以上使用された場合にのみ、ジェネリックスが適用されます。

**(4) ジェネリックはネストできます。 **

type パラメータには別のジェネリック型を使用できます。

type OrNull<Type> = タイプ|null;

type OneOrMany<Type> = Type|Type[];

type OneOrManyOrNull<タイプ> = OrNull<OneOrMany<タイプ>>;

上の例では、最後の行のジェネリック OrNull の型パラメータは、別のジェネリック OneOrMany です。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0