一.簡介
TypeScript 就引入了“泛型”(generics)。泛型的特點就是帶有“類型參數”(type parameter)。
在日常 TypeScript 編程中,我們經常會遇到這樣的場景:函數的參數類型與返回值類型密切相關。此時,泛型(Generics)就成為了我們編寫靈活、高復用性代碼的重要工具。
來看一個例子來明白泛型的重要性:
function getFristValue(arr: number[]): any {return arr[0]; // 返回數組的第一個數據
}
這段代碼雖然工作正常,但 any
類型 丟失了類型之間的聯系。我們無法知道傳入的是 string[]
還是 number[]
,返回值類型也就不明確了。
于是,我們使用泛型來表達這種“輸入與輸出類型相關”的關系:
function getFirst<T>(arr: T[]): T {return arr[0];
}
這里的 <T>
就是類型參數,它類似于函數中的變量,調用函數時再決定 T 的具體類型。比如:
function getFirst<T>(arr: T[]): T {return arr[0];
}getFirst<number>([1, 2, 3]); // 返回 number 類型
getFirst(["a", "b", "c"]); // 推斷為 string 類型
二.泛型的寫法
1.function函數
function
關鍵字定義的泛型函數,類型參數放在尖括號中,寫在函數名后面。
function fun<T>(a: T): T {return a;
}console.log(fun<number>(1));
那么對于變量形式定義的函數,泛型有下面兩種寫法。
let my_function: <T>(a: T) => T = function <T>(a: T): T {return a;
};let you_function: <typr>(a: typr) => typr = function <typr>(a: typr): typr {return a;
};my_function<number>(10); // 10
you_function<string>("hello"); // "hello"
2.interface接口
interface Box<T> {contents: T;
}let box: Box<number> = { contents: 123 };
3.class類
class Pair<K, V> {constructor(public key: K, public value: V) {}
}const kv = new Pair<string, number>("age", 30);
也可以設置默認值:
class Generic<T = string> {list: T[] = [];add(item: T) {this.list.push(item);}
}const g = new Generic();
g.add("hello"); // 正確
g.add(123); // 報錯
注意:泛型類不能使用類型參數定義靜態屬性。
class Example<T> {static prop: T; // 報錯
}
4.type類型別名
type Nullable<T> = T | null | undefined;type Container<T> = { value: T };const a: Container<number> = { value: 42 };
三.類型參數的默認值
類型參數可以設置默認值。使用時,如果沒有給出類型參數的值,就會使用默認值。
function getFirst<T = string>(arr: T[]): T {return arr[0];
}
上面示例中,T = string
表示類型參數的默認值是string
。調用getFirst()
時,如果不給出T
的值,TypeScript 就認為T
等于string
。
若調用時未顯式提供類型,TypeScript 會自動推斷,但默認值只有在無法推斷時才會生效。
一旦類型參數有默認值,就表示它是可選參數。如果有多個類型參數,可選參數必須在必選參數之后。
function combine<T, U, V = boolean>(a: T, b: U, c: V): [T, U, V] {return [a, b, c]; }const res1 = combine(1, "hello", true); // [number, string, boolean] const res2 = combine("a", 2, false); // [string, number, boolean] const res3 = combine("x", 3, undefined); // [string, number, boolean]
四.數組的泛型表示
TypeScript 原生的數據結構,如數組、Map、Set、Promise 都是泛型結構:
《數組》一章提到過,數組類型有一種表示方法是Array<T>
。這就是泛型的寫法,Array
是 TypeScript 原生的一個類型接口,T
是它的類型參數。聲明數組時,需要提供T
的值。
let arr: Array<number> = [1, 2, 3];
上面的示例中,Array<number>
就是一個泛型,類型參數的值是number
,表示該數組的全部成員都是數值。
同樣的,如果數組成員都是字符串,那么類型就寫成Array<string>
。事實上,在 TypeScript 內部,數組類型的另一種寫法number[]
、string[]
,只是Array<number>
、Array<string>
的簡寫形式。
在 TypeScript 內部,Array
是一個泛型接口,類型定義基本是下面的樣子。
interface Array<Type> {length: number;pop(): Type | undefined;push(...items: Type[]): number;// ...
}
其他的 TypeScript 內部數據結構,比如Map
、Set
和Promise
,其實也是泛型接口,完整的寫法是Map<K, V>
、Set<T>
和Promise<T>
。
TypeScript 默認還提供一個ReadonlyArray<T>
接口,表示只讀數組。
function doStuff(values: ReadonlyArray<string>) {values.push("hello!"); // 報錯
}
上面示例中,參數values
的類型是ReadonlyArray<string>
,表示不能修改這個數組,所以函數體內部新增數組成員就會報錯。因此,如果不希望函數內部改動參數數組,就可以將該參數數組聲明為ReadonlyArray<T>
類型。
五.類型參數的約束條件
TypeScript 提供了一種語法,允許在類型參數上面寫明約束條件,如果不滿足條件,編譯時就會報錯。這樣也可以有良好的語義,對類型參數進行說明。
function comp<T extends { length: number }>(a: T, b: T) {if (a.length >= b.length) {return a;}return b;
}
上面示例中,T extends { length: number }
就是約束條件,表示類型參數 T 必須滿足{ length: number }
,否則就會報錯。
comp([1, 2], [1, 2, 3]); // 正確
comp("ab", "abc"); // 正確
comp(1, 2); // 報錯
上面示例中,只要傳入的參數類型不滿足約束條件,就會報錯。
類型參數的約束條件采用下面的形式。
<TypeParameter extends ConstraintType>
上面語法中,TypeParameter
表示類型參數,extends
是關鍵字,這是必須的,ConstraintType
表示類型參數要滿足的條件,即類型參數應該是ConstraintType
的子類型。
類型參數可以同時設置約束條件和默認值,前提是默認值必須滿足約束條件。
常見的約束類型
約束類型 示例 含義說明 { length: number }
T extends { length: number }
要求有 length
屬性string
、number
等原始類型T extends string
只能傳入對應類型 自定義接口或類 T extends Person
要求 T 是 Person
類型或其子類聯合類型 T extends string | number
T 只能是 string
或number
六.注意點
1.盡量少用泛型。
泛型雖然靈活,但是會加大代碼的復雜性,使其變得難讀難寫。一般來說,只要使用了泛型,類型聲明通常都不太易讀,容易寫得很復雜。因此,可以不用泛型就不要用。
2.類型參數越少越好。
多一個類型參數,多一道替換步驟,加大復雜性。因此,類型參數越少越好
3.類型參數需要出現兩次。
如果類型參數在定義后只出現一次,那么很可能是不必要的。
4.泛型可以嵌套。
類型參數可以是另一個泛型。