在先前的章節實作猜數字程式的時候,我們有用到Promise<string>
這樣的型別。在這個型別中,由小於<
和大於>
符號括起來的部份稱為「泛型」(generic)。在「類型論」(type theory)中,「泛型」就是「參數多型」(parametric polymorphism),在定義型別的時候不去明確地指定具體的型別,而以參數的形式來傳入型別,如此一來擁有泛型的類別和函數只需要實作一次,就可以透過參數傳入不同型別的方式來將其應用在各種型別的資料下。
類別泛型
我們就實作出一個模擬堆疊(stack)空間的類別來練習泛型吧!程式碼如下:
class Stack {
private data: any[] = [];
push(element: any) {
this.data.push(element);
}
pop() {
return this.data.pop();
}
}
以上實作了一個Stack
類別,這個類別在被實體化之後會擁有一個私有的陣列,並提供push
和pop
方法來操作這個陣列。
我們可以撰寫出以下程式,來將任意型別的值透過push
方法存進Stack
類別的物件中,再用pop
方法將這些值取出。
class Stack {
private data: any[] = [];
push(element: any) {
this.data.push(element);
}
pop() {
return this.data.pop();
}
}
const stack = new Stack();
stack.push(1);
stack.push("2");
stack.push(true);
console.log(stack.pop());
console.log(stack.pop());
console.log(stack.pop());
以上程式的執行結果如下:
2
1
目前的Stack
類別物件可以存入任意型別的值,如果我們想要限制值的型別該怎麼做呢?最直覺的方式就是利用前面的章節提到的物件轉型方式,將Stack
物件的型別轉成其它型別。如下:
class Stack {
private data: any[] = [];
push(element: any) {
this.data.push(element);
}
pop() {
return this.data.pop();
}
}
const numberStack: { push: (element: number) => void, pop: () => number } = new Stack();
const stringStack: { push: (element: string) => void, pop: () => string } = new Stack();
const booleanStack: { push: (element: boolean) => void, pop: () => boolean } = new Stack();
numberStack.push(1);
stringStack.push("2");
booleanStack.push(true);
console.log(booleanStack.pop());
console.log(stringStack.pop());
console.log(numberStack.pop());
這種作法雖然可行,但它不僅麻煩又很不安全。所以泛型的存在就很重要了,我們可以利用泛型,將以上程式改寫為:
class Stack<T> {
private data: T[] = [];
push(element: T) {
this.data.push(element);
}
pop() {
return this.data.pop();
}
}
const numberStack = new Stack<number>();
const stringStack = new Stack<string>();
const booleanStack = new Stack<boolean>();
numberStack.push(1);
stringStack.push("2");
booleanStack.push(true);
console.log(booleanStack.pop());
console.log(stringStack.pop());
console.log(numberStack.pop());
在Stack
類別名稱後加上泛型參數T
,就可以使用泛型參數T
來代替類別內部用到的型別。使用new
關鍵字來產生類別的實體物件時,若沒有明確定義泛型參數的型別(例如只寫new Stack()
,而不寫new Stack<number>()
),且也沒有在建構子的參數中被用到的泛型參數,編譯器會自動將那些泛型參數推論為unknown
(稍候會介紹unknown
與any
的差異)。換句話說,如果在建構子參數中有用到泛型參數,則編譯器會根據建構子被呼叫時實際傳入參數的值的型別來自動推論泛型參數的實際型別。
一個型別可以使用多個泛型參數,用逗號,
隔開即可。例如:
class Stack2D<X, Y> {
private data: [X, Y][] = [];
push(element: [X, Y]) {
this.data.push(element);
}
pop() {
return this.data.pop();
}
}
const numberStack = new Stack2D<number, number>();
const stringStack = new Stack2D<string, string>();
const booleanStack = new Stack2D<boolean, boolean>();
const numberStringStack = new Stack2D<number, string>();
const stringNumberStack = new Stack2D<string, number>();
numberStack.push([1, 2]);
stringStack.push(["1", "2"]);
booleanStack.push([true, false]);
numberStringStack.push([1, "2"]);
stringNumberStack.push(["1", 2]);
console.log(stringNumberStack.pop());
console.log(numberStringStack.pop());
console.log(booleanStack.pop());
console.log(stringStack.pop());
console.log(numberStack.pop());
unknown
與any
型別的差異
unknown
和any
型別有點相像,它們都可以用來儲存任意型別的值。
如下:
const a1: any = 1;
const a2: any = "2";
const a3: any = false;
const u1: unknown = 1;
const u2: unknown = "2";
const u3: unknown = false;
差異之處在於,any
型別的值可以被直接轉型成其它任意型別,unknown
型別的值則需要靠as
關鍵字來強制轉型。
如下:
const a1: any = 1;
const a2: number = a1;
const u1: unknown = 1;
const u2: number = u1 as number;
const u3: number = u1; // compilation error
另外,unknown
型別的值無法直接作為運算元、物件或是函數,必須要先轉型成其它可用型別才行。例如:
const a1: any = 1;
const a2: any = 2;
const a3 = a1 + a2;
const u1: unknown = 1;
const u2: unknown = 2;
const u3 = u1 as number + (u2 as number);
const u4 = u1 + u2; // compilation error
介面泛型
介面可以使用泛型。舉例來說:
interface Point<T> {
x: T,
y: T
}
class Vertex<T> implements Point<T> {
x: T;
y: T;
constructor(x: T, y: T) {
this.x = x;
this.y = y;
}
}
函數泛型
函數也可以使用泛型,例如:
function reverse(items: any[]): any[] {
const output = [];
for (let i = items.length - 1;i >= 0;i--) {
output.push(items[i]);
}
return output;
}
const a = [1, 3, 5, 7];
const a_rev = reverse(a);
a_rev[0] = "1";
以上的reverse
函數,可以傳入任意型別的陣列,並將其反向。
我們可以發現,由於reverse
函數回傳的值的型別為any[]
,而把原先的型別約束破壞掉了。替reverse
函數加上泛型,就可以輕易地解決這個問題。程式如下:
function reverse<T>(items: T[]): T[] {
const output = [];
for (let i = items.length - 1;i >= 0;i--) {
output.push(items[i]);
}
return output;
}
const a = [1, 3, 5, 7];
const a_rev = reverse(a);
a_rev[0] = "1"; // compilation Error
以上程式第10行,可以不必將呼叫reverse
的表達式寫成reverse<number>(a)
,因為TypeScript的編譯器會自動推論泛型參數T
所代表的實際型別。(由於變數a
的型別為number[]
,因此泛型參數T
會被自動推論為number
。)
匿名函數也可以使用泛型,程式如下:
const reverse = function <T> (items: T[]): T[] {
const output = [];
for (let i = items.length - 1;i >= 0;i--) {
output.push(items[i]);
}
return output;
};
const a = [1, 3, 5, 7];
const a_rev = reverse(a);
a_rev[0] = "1"; // compilation Error
Lambda語法的匿名函數也可以使用泛型,程式如下:
const reverse = <T>(items: T[]): T[] => {
const output = [];
for (let i = items.length - 1;i >= 0;i--) {
output.push(items[i]);
}
return output;
};
const a = [1, 3, 5, 7];
const a_rev = reverse(a);
a_rev[0] = "1"; // compilation Error
泛型參數的預設值
前面有提到如果類別的泛型參數沒有明確定義出來,且也未在建構子的參數中被使用到的話,就會被推論為unknown
。其實這點在使用有泛型的函數時也會發生,只不過比較少見。例如:
function f<T>(): T[] {
return [];
}
const o = f();
o.push(1);
o.push("2");
以上的泛型參數T
並未被用在f
函數的參數中,所以會被編譯器自動推論為unknown
。
如果要讓編譯器可以將這類的泛型參數自動推論為其它我們指定的型別,而不是any
的話,可以在泛型參數名稱後面加上=
字元,再接上型別。
例如:
function f<T = number>(): T[] {
return [];
}
const o = f();
o.push(1);
o.push("2"); // compilation Error
泛型參數的繼承限定
在定義型別參數的時候,可以使用extends
關鍵字來限定這個型別參數實際的型別一定要是哪一個型別,或是其子型別。
例如:
interface PointX {
x: number
}
interface PointY {
y: number
}
interface PointZ {
z: number
}
interface Point2D extends PointX, PointY {
}
interface Point3D extends Point2D, PointZ {
}
class Vertex implements Point3D {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
}
function getX<T extends PointX>(vertex: T): number {
return vertex.x;
}
function getY<T extends PointY>(vertex: T): number {
return vertex.y;
}
function getZ<T extends PointZ>(vertex: T): number {
return vertex.z;
}
const v = new Vertex(1, 2, 3);
console.log(getX(v));
console.log(getY(v));
console.log(getZ(v));
以上程式的輸出結果如下:
2
3
extends
關鍵字後面可以加上keyof
關鍵字,再加上一個物件型別,就表示要將泛型參數限定為這個物件型別的屬性鍵值的型別或是該型別的子型別。例如:
interface PointX {
x: number
}
interface PointY {
y: number
}
interface PointZ {
z: number
}
interface Point2D extends PointX, PointY {
}
interface Point3D extends Point2D, PointZ {
}
class Vertex implements Point3D {
x: number;
y: number;
z: number;
constructor(x: number, y: number, z: number) {
this.x = x;
this.y = y;
this.z = z;
}
}
function getX<T extends PointX>(vertex: T): number {
return vertex.x;
}
function getY<T extends PointY>(vertex: T): number {
return vertex.y;
}
function getZ<T extends PointZ>(vertex: T): number {
return vertex.z;
}
function get<T extends Vertex, K extends keyof Vertex>(vertex: T, key: K): number {
return vertex[key];
}
事實上,泛型所用的extends
和keyof
關鍵字是可以直接被用在型別中的,這個我們會在之後的章節詳細介紹。
總結
在這個章節中我們學會了TypeScript提供的泛型功能,它可以讓我們的程式看起來寫簡潔,用起來也更安全。下一個章節,我們要來更深入地探討TypeScript的型別。
下一章:進階的型別用法。