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 つのパラメータ arr1
、arr2
と戻り値は同じ型です。 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 つの型パラメータ T
と U
を持つグローバル関数に書き換えます。意味は、元の配列の型が 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>
です。ここで、T
は createInstance( )
type パラメータは、関数が呼び出されるときに特定の型を指定します。
ジェネリック クラスは、静的プロパティと静的メソッドを除いて、クラス自体で定義されているため、クラスのインスタンスを記述することに注意してください。したがって、型パラメータを参照することはできません。
クラス C<T> {
静的データ: T; // エラーレポート
コンストラクター(公開値:T) {}
}
上記の例では、静的属性 data
は型パラメータ T
を参照していますが、型パラメータはインスタンス プロパティとインスタンス メソッドにのみ使用できるため、これは許可されず、エラーが報告されます。
型エイリアスの一般的な記述
type コマンドで定義された型エイリアス、ジェネリックも使用できます。
型 Nullable<T> = T 未定義 |
上の例では、Nullable<T>
はジェネリック型です。型を渡す限り、この型の unknown
と null
の共用体型を取得できます。
別の例を示します。
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 は T
が string
に等しいと想定します。
ただし、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 つの型パラメータ T
と U
があります。 「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 つが一貫しているかどうかをチェックします。
Map
、Set
、Promise
などの他の 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"]
上の例では、型パラメータ A
と B
の両方に制約があり、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