目次
序文
この記事はTypeScript 知識のまとめ記事シリーズに含まれています。修正は歓迎です。
前回の記事 では、ジェネリックの柔軟性と能力について学び、ジェネリックの基本的な使用法と一般的な使用法について学びました。この記事では、ジェネリックの他の使用法についていくつかの高度な拡張を行います。以前の記事で紹介できる知識ポイントは多数ありますが、ジェネリックと合わせて理解するとよりよく理解できると思いますので、苦労せずに直接始めましょう
一般的な制約
ジェネリック制約を使用してジェネリック パラメータのタイプを制限し、特定のタイプの要件を満たすようにすることができます。ジェネリック制約は、キーワード拡張の後にタイプを追加することによって記述されます。制約のタイプは任意のタイプにすることができます。以下は簡単な例です。
type Animal<T extends hobbyList> = {
name: string
hobby: T
}
type hobbyList = {
length: number
push: (...args: any[]) => number
}
const animal: Animal<hobbyList> = {
name: "阿黄",
hobby: ['ball', 'flying-disc']
}
animal.hobby.push("run")
console.log(animal.hobby); // [ 'ball', 'flying-disc', 'run' ]
上記の関数では、animal から渡されるジェネリック T に length 属性と Push メソッドのみが含まれるように制限していることがわかりますが、このとき、animal で趣味の種類を変更したり、他のメソッドを呼び出したりすると、エラーが報告されます。
animal.hobby.concat(["run"]) // 类型“hobbyList”上不存在属性“concat”
animal.hobby = {} // 类型“{}”缺少类型“hobbyList”中的以下属性: length, push
共用体型 + 汎用制約
前の記事では、ジェネリック コンストレイントにも適用できるジョイント タイプについて説明しました。このシナリオでは、ジェネリック タイプ パラメーターは、次のような複数のタイプの 1 つである必要があることを意味します。
type Animal<T extends string | string[]> = {
name: string
hobby: T
}
const animal: Animal<string> = {
name: "阿黄",
hobby: "ball"
}
const animal2: Animal<string[]> = {
name: "阿黄",
hobby: ['ball', 'flying-disc']
}
クロスタイプ + 一般的な制約
上記のジョイント タイプに加えて、クロス タイプを使用してジェネリック タイプを制限し、複数のタイプ特性を同時に満たすこともできます。
type Animal<T extends arrayLength & arrayPush> = {
name: string
hobby: T
}
type arrayLength = {
length: number
}
type arrayPush = {
push: (...args: any[]) => number
}
type MyArray<T> = arrayLength & arrayPush & {
forEach: (cb: (item: T, i: number, arr: MyArray<T>) => void) => void
}
const animal: Animal<MyArray<string>> = {
name: "阿黄",
hobby: ['ball', 'flying-disc']
}
animal.hobby.push("run")
console.log(animal.hobby.length); // 3
上記のコードでは、MyArray 型が arrayPush と arrayLength の 2 つのインターフェイスを同時に展開し、それを元に forEach メソッドを追加していますが、このとき、Animal のジェネリック型に正しく割り当てることができます。
ジェネリック制約 ジェネリック
いわゆるジェネリック制約ジェネリックは、ジェネリック型パラメータ内で他のジェネリック型パラメータを使用してその型を制約するもので、これは入れ子の人形に少し似ています。たとえば、上記のコードにいくつかの変更を加えます: getHobby 属性を Animal に追加します。属性の型は U (つまり、arrayLength) で、T は U に制約されているため、T は引き続き MyArray<string> を取ることができます。簡単に言うと、T 型は U 型のサブクラスであり、サブクラスの属性は多かれ少なかれしかありえません
type Animal<T extends U, U> = {
name: string
hobby: T
getHobby: U
}
type arrayLength = {
length: number
}
type arrayPush = {
push: (...args: any[]) => number
}
type MyArray<T> = arrayLength & arrayPush & {
forEach: (cb: (item: T, i: number, arr: MyArray<T>) => void) => void
}
const animal: Animal<MyArray<string>, arrayLength> = {
name: "阿黄",
hobby: ['ball', 'flying-disc'],
getHobby: {
length: 2
}
}
animal.hobby.push("run")
console.log(animal.getHobby.length);// 2
console.log(animal.hobby.length); // 3
再帰型エイリアス
オブジェクト インターフェイスと型エイリアスを記述するとき、プロトタイプ チェーンやメニューのサブ項目など、ツリー構造やネストされたオブジェクトに遭遇することがあります。このとき、次のような型で自分自身を呼び出すことができる再帰的構造が必要です。
type MenuItem = {
label: string
key: string
url: string
}
type Menu<T> = {
value: T
children?: Menu<T>[]
}
const menu: Menu<MenuItem> = {
value: {
label: '菜单1',
key: 'menu1',
url: '/menu1'
},
children: [
{
value: {
label: '子菜单1',
key: 'child1',
url: '/child1'
},
children: [
]
}, {
value: {
label: '子菜单2',
key: 'child2',
url: '/child2'
},
children: [
]
},
]
}
上記のコードでは、再帰型エイリアスを使用して単純な 2 次メニューを実装しました。
条件タイプ
JS では、私たちは皆、三項演算子 a ? b : c を使用したことがあります。TS にはこのような書き方もあります。これを条件型と呼びます。これは、型パラメータの属性や、次のような他の型情報に従って型の一部を選択できます。
type ReturnObject<T> = T extends { [key: string]: any } ? T : null
type isNotObj = ReturnObject<false>
type isObj = ReturnObject<{ name: "张三" }>
上記のコードでは、オブジェクト型の制約を実装しました。渡された型がオブジェクト型の場合はその型を返し、それ以外の場合は null を返します。
分布条件タイプ
TS で条件タイプをディスパッチすると、複雑なタイプ変換がより小さなタイプに分割され、最終的にそれらが結合される可能性があります。
たとえば、条件付き型を使用して単純な型チェッカーを実装します。数値、文字、またはブール型の場合はそれぞれの文字列を取得し、それ以外の場合は他の文字列を返します。
type IGetType<T> = T extends string ? 'str'
: T extends number ? 'num'
: T extends boolean ? 'bool'
: 'other'
type INum = IGetType<number>// num
type IBool = IGetType<boolean>// bool
type IStr = IGetType<string>// str
type IOther = IGetType<unknown>// other
タイプフィルター
名前が示すように、型フィルタリングはコレクション内の適格な型を除外することです。
上記の概念に基づいて、インクルード型チェックを実装できます。次の関数では、文字、数値、およびブール型のみを通過させる型フィルターを実装します。
type MyInclude<T, U> = U extends T ? U : never;
type whiteList = string | number | boolean // 允许的类型
type getString = MyInclude<whiteList, string> // string
type getArray = MyInclude<whiteList, string[]> // never
型推論
キーワードを推測する
型導出を理解する前に、infer キーワードについて理解しておく必要があります。このようなMRはTS 2.8 バージョンで登場しました。
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
上記のコードの infer R は何を意味しますか? まずはユースケースを見てみましょう
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
type IGetStr = MyReturnType<() => string> // string
type IGetNum = MyReturnType<() => number> // number
type IStr = MyReturnType<string>// string
わかりますか?上記のコードの推論は、関数の戻り値を R に抽出します。関数の型を渡すと、MyReturnType 型は関数の戻り値を返します。それ以外の場合は、元の型を返します。
次のタイプの実装を検討してください。
type MyArrayItem<T> = T extends (infer Item)[] ? Item : T;
type IStr = MyArrayItem<string>// string
type INumArr = MyArrayItem<number[]>// number
上記のコードでは、配列のサブ項目の型を抽出する関数を実装しています。
型推論に戻る
実際、型推定とは関数のパラメータの型に応じて関数の戻り値の型を推定することであり、上記の MyReturnType を使用して関数の戻り値の型を推定します。
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
const concatStr = (str1: string, str2: string) => {
return str1 + str2
}
const addNum = (num1: number, num2: number) => {
return num1 + num2
}
type concatStrReturn = MyReturnType<typeof concatStr> // string
type concatNumReturn = MyReturnType<typeof addNum> // number
マッピングとインデックスの種類
前の記事では、keyof キーワードと in キーワードを紹介し、マッピング タイプとインデックス タイプを使用してオブジェクト タイプをコピーし、属性値を文字列に設定しました。
type IAnimal = {
name: string
age: number
hobby: string[]
}
type IDog = {
[key in keyof IAnimal]: string
}
インデックスアクセスタイプ
JavaScript では、オブジェクトのインデックスを使用してオブジェクト プロパティを取得します。Object.key または Object['key'] を使用してオブジェクトのキー プロパティを取得します。一方、TypeScript では、Object[key] を使用してオブジェクト タイプのキー プロパティを取得できます。
したがって、オブジェクトのプロパティを取得する型を書くことができます。
interface IAnimal {
name: string
age: number
hobby: string[]
}
type GetItem<T, K extends keyof T> = T[K]
type AnimalName = GetItem<IAnimal, 'name'>// string
type AnimalAge = GetItem<IAnimal, 'age'>// number
マッピングタイプ
上記のコードに基づいて、IAnimal[key] をさらに拡張して、IAnimal の各属性を表すことができます。このキーは、名前 | 年齢 | 趣味 というオブジェクト属性名 (ジョイント タイプ) のコレクションとして理解できます。
type IDog = {
[key in keyof IAnimal]: IAnimal[key]
}
上記のコードはIAnimalの各項目を表しています。
この記述方法により、IAnimal をジェネリック型に抽出し、一般的な型のエイリアス関数を記述して、オブジェクトの各プロパティを走査して読み取り専用に設定するという目的を達成できます。
type ReadonlyObject<T> = { readonly [key in keyof T]: T[key] };
やってみよう
type IAnimalReadonly = ReadonlyObject<IAnimal>
/*
等同于
type IAnimalReadonly = {
readonly name: string;
readonly age: number;
readonly hobby: string[];
}
*/
必須の属性
TSのマッピングタイプにはオプション属性が多数ありますが、一括して必須属性に変更するにはどうすればよいでしょうか?
現時点では、この目的を達成するために、次のように属性名に「-?」記号を追加できます。
type IAnimal = {
name?: string
age?: number
hobby?: string[]
}
type IDog = {
[key in keyof IAnimal]-?: string
}
この時、IDogの各項目は必須属性となります。
変更可能な属性
読み取り専用属性 readonly に対応するのは変数属性であり、この効果を実現するために属性名の前に -readonly を追加することにより、前述の必須属性に似ています。
type IAnimal = {
readonly name: string
readonly age: number
readonly hobby: string[]
}
type Mutable<T> = {
-readonly [key in keyof T]: string
}
type IAni = Mutable<IAnimal>
エピローグ
以上が記事の内容ですが、この記事ではジェネリックスの高度な使い方に焦点を当て、主にジェネリックスの制約、再帰型の別名、条件付き型、マッピング、インデックス型について説明します。とその詳しい使い方
最後まで読んでいただきありがとうございます。記事が役に立った場合は、サポートをお願いします。