說一下爲何要寫這篇文章,筆者近三個月內開始接觸並使用 TypeScript 開發項目,一開始總以爲多餘又耗費時間,好在筆者有一個優勢就是算是願意愛折騰愛學習,無論有用沒用~html
近來發現 TypeScript 已成一種趨勢,基本已成大型項目的標配。TypeScript 彌補了弱類型的 JavaScript 所帶來的一些缺點,能夠幫助咱們構建更穩健的代碼,同時也加強可閱讀性和可維護性。可使得許多運行時才能出現的錯誤,在編譯時就暴露出來,讓潛在的問題更容易發現。git
筆者在學習 TypeScript 的過程當中,以爲 TypeScript 比較難以理解和須要花費時間的點,就是泛型以及相關特性了,好比條件推斷 infer 等(固然,也是頗有意思的一部分)。這篇文章就總結並和你們分享一下一些相關知識~github
TypeScript 中泛型設計的目的是使在成員之間提供有意義的約束,爲代碼增長抽象層和提高可重用性。泛型能夠應用於 Typescript 中的函數(函數參數、函數返回值)、接口和類(類的實例成員、類的方法)。typescript
先來看這個若是日常咱們寫函數的參數和返回值類型可能會這麼寫~約束了函數參數和返回值必須爲數字類型。編程
function identity(arg: number): number {
return arg;
}
複製代碼
那麼問題來了。若是我要參數和返回值類型限定爲字符串類型的話,又改爲這麼寫。數組
function identity(arg: string): string {
return arg;
}
複製代碼
不科學呀!當函數想支持多類型參數或返回值的時候,上述寫法將變得十分不靈活。因而泛型就閃亮登場了!安全
考慮如下寫法:ide
function identity<T>(arg: T): T {
return arg;
}
複製代碼
function identities<T, U>(arg1: T, arg2: U): [T, U] {
return [arg1, arg2];
}
複製代碼
使用泛型後,能夠接受任意類型,可是又完成了函數參數和返回值的約束關係。十分靈活~可複用性大大加強了!函數式編程
有時候咱們定義的泛型不想過於靈活或者說想繼承某些類等,能夠經過 extends 給泛型加上約束。函數
interface ILengthwise {
length: number;
}
function loggingIdentity<T extends ILengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
複製代碼
其實泛型咱們在 React 組件裏也很常見(說不定你們以爲很眼熟了),用泛型確保了 React 組件的 Props 和 State 是類型安全的~
interface ICustomToolProps {
// @TODO
}
interface ICustomToolState {
// @TODO
}
class CustomTool extends React.Component<ICustomToolProps, ICustomToolState> {
// @TODO
}
複製代碼
因此你們看上面的 ICustomToolProps、ICustomToolState 其實也是泛型。應用在類上面的泛型語法簡化以下示例:
class Directive<T> {
private name: T;
public getName(): T {
return this.name;
}
// @TODO
}
複製代碼
當使用泛型時,通常狀況下經常使用 T、U、V 表示,若是比較複雜,應使用更優語義化的描述,好比上述 React 組件示例。
好比說設計一個指令管理者對象~用來管理指令
enum EDirective {
Walk = 1,
Jump = 2,
Smile = 3
}
class DirectiveManager<T> {
private directives: Array<T> = [];
add = (directive: T): Array<T> => {
this.directives = this.directives.concat(directive);
return this.directives;
};
get = (index: number): T => {
return this.directives[index];
};
shift = (): Array<T> => {
this.directives = this.directives.slice(1);
return this.directives;
};
// @TODO
}
複製代碼
初始化一個指令管理者的實例。給定泛型爲 number 類型。
能夠發現指令管理者對象成功被限定類型,若是傳參類型錯誤,會被 TypeScript 及時提醒。
通過上面的介紹,相信你們都對泛型有必定了解了!那麼接下來經過帶你們看 JavaScript 數組方法的泛型來加深理解~
咱們來閱讀如下數組對象的屬性以及方法的泛型(我抽取了一部分,但願你們不要以爲代碼過長,就略過不讀,我以爲也是換一種方式熟悉 JavaScript 語法的一種方式~)
interface Array<T> {
length: number;
[n: number]: T;
reverse(): T[];
shift(): T;
pop(): T;
unshift(...items: T[]): number;
push(...items: T[]): number;
slice(start?: number, end?: number): T[];
sort(compareFn?: (a: T, b: T) => number): T[];
indexOf(searchElement: T, fromIndex?: number): number;
lastIndexOf(searchElement: T, fromIndex?: number): number;
every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean;
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): T[]; splice(start: number): T[]; splice(start: number, deleteCount: number, ...items: T[]): T[]; concat<U extends T[]>(...items: U[]): T[]; concat(...items: T[]): T[]; reduce( callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T ): T; reduce<U>( callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U ): U; reduceRight( callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue?: T ): T; reduceRight<U>( callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U ): U; } 複製代碼
相信你們對數組方法都十分熟悉了~下面將帶你們稍微看一下部分方法
shift(): T;
pop(): T;
unshift(...items: T[]): number;
push(...items: T[]): number;
複製代碼
平時你們可能會混淆幾個方法。可是看了它們的函數簽名後,是否以爲一目瞭然。push/unshift 方法調用後返回時數字類型,也就是其數組長度。而 shift/pop 方法調用後返回了彈出的元素,
forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; 複製代碼
這兩個方法很值得一說,由於二者都具有遍歷的特徵,因此常見不少同窗們混用這兩個方法,其實大有講究。看到 forEach 的方法實際上是返回 void 的,而在 map 方法裏,最終是將 T[] 映射成了 U[]。因此呢,一言以蔽之,forEach 通常用來執行反作用的,好比持久的修改一下元素、數組、狀態等,以及打印日誌等,本質上是不純的。而 map 方法用來做爲值的映射,本質上是純淨的,在函數式編程裏十分重要。
splice、concat、reduce、reduceRight 這些方法基本都重載了兩次,也就明顯告訴咱們這些方法是有多種傳參調用方式的。
好比concat<U extends T[]>(...items: U[]): T[];
這裏使用到了上述和你們介紹的泛型約束,意思爲能夠傳遞多個數組元素。下面緊跟着的concat(...items: T[]): T[];
則告訴咱們也能夠傳遞多個元素。兩個函數簽名都告訴咱們函數返回一個數組,它由被調用的對象中的元素組成,每一個參數的順序依次是該參數的元素(若是參數是數組)或參數自己(若是參數不是數組)。它不會遞歸到嵌套數組參數中。
有時候咱們有從舊類型中建立新類型的一個需求場景,TypeScript 提供了映射類型這種方式。 在映射類型裏,新類型以相同的形式去轉換舊類型裏每一個屬性
好比咱們將每一個屬性成爲 readonly 類型,如圖
type Readonly<T> = { readonly [P in keyof T]: T[P] };
複製代碼
同理以下,見圖可理解~
type Partial<T> = { [P in keyof T]?: T[P] };
複製代碼
那麼你們應該也 get 到下述代碼的意圖了~
type Nullable<T> = { [P in keyof T]: T[P] | null };
複製代碼
擴展一下能夠寫任意的映射類型來知足本身的需求場景~
enum EDirective {
Walk = 1,
Jump = 2,
Smile = 3
}
type DirectiveKeys = keyof typeof EDirective;
type Flags = { [K in DirectiveKeys]: boolean };
複製代碼
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends string, T> = { [P in K]: T };
複製代碼
infer 表示在 extends 條件語句中待推斷的類型變量。
在條件類型的 extends 語句中,咱們能夠用 infer 聲明一個類型變量,而後在其分支語句中使用該類型變量。若是不懂,沒有關係,請繼續看下面的例子~
該語句中的(param: infer P)
,爲函數首個參數推斷聲明瞭一個類型變量 P,若是泛型 T 是一個函數,則根據以前的類型變量 P,提取其推斷的函數參數並返回,不然返回原有類型。
type ParamType<T> = T extends (param: infer P) => any ? P : T;
複製代碼
如圖因此,成功提取了 IPrint 的參數類型。
同理以下,提取返回值一樣理解~
type ReturnType<T> = T extends (...args: any[]) => infer P ? P : any;
複製代碼
下述代碼能夠提取構造函數參數類型~
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (
...args: infer P
) => any
? P
: never;
複製代碼
T extends new (...args: any[]) => any
這裏用到了泛型約束,new (...args: infer P)
這一句將參數推斷聲明爲類型變量 P。剩餘的仍是同樣的理解~
下述提取實例類型~(和提取構造函數參數類型小有不一樣~同窗們本身發現一下)
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R
? R
: any;
複製代碼
剩餘的列舉一些比較實用的,參照上述方式理解,同窗們如若感興趣,可自行谷歌~
type Flatten<T> = T extends (infer U)[] ? U : T;
複製代碼
type Unpromisify<T> = T extends Promise<infer R> ? R : T;
複製代碼
type ElementOf<T> = T extends Array<infer E> ? E : never;
複製代碼
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (( k: infer I ) => void) ? I : never; 複製代碼
泛型在編譯期間被刪除,所以不要在泛型函數中寫 typeof T、new T、instanceof T。
總的來講,在一箇中大型項目裏採用 TypeScript 目前看來仍是十分有價值的,能夠在開發過程當中給到更多約束,從而大大減小運行時的錯誤。筆者建議你們能夠多多學習 TypeScript,從而寫出更加工整,健壯的代碼。
而此篇文章介紹的泛型和條件推斷可讓你們寫出更加靈活,具備可擴展性的TypeScript類型哈哈哈哈~
以上~謝謝你們的閱讀,如對你們有所助益,不勝榮幸~