學習資源
什麼是泛型(Generics)?
在第一週第四天有提到過用泛型表示法來定義陣列的型別,那究竟什麼是泛型呢?
泛型是一種程式設計概念,它讓你在定義函式、介面或類別時,不預先指定具體的型別,而是在使用的時候再指定具體型別的一種特性。這讓開發者能夠在不失去型別安全的情況下,撰寫更靈活、可重用的程式碼。泛型通常在靜態型別語言(如TypeScript、Java 和 C#)中使用。
以 TypeScript 為例,泛型的語法是在函式名稱、類別名稱或介面名稱後面使用尖括號(<T>
),其中 T
通常表示型別變數(Type Variable)。然後,你可以在函式、類別或介面的實作中使用這個型別變數,而不是具體的型別。
泛型變數命名慣例上使用
T
、U
、V
等大寫字母,但實際上可以使用任意的變數名稱。
以下是 TypeScript 的泛型範例:
// 傳入跟回傳值會是同個型別,用型別變數 T 來代表
function identity<T>(arg: T): T {
return arg;
}
// 使用函式時可以指定不同的型別
let outputString = identity<string>("myString");
let outputNumber = identity<number>(7);
在這個範例中,identity
函式是泛型函式,它接受一個型別變數 T
。該函式的功能是回傳與其輸入相同的值。當我們使用這個函式時,可以指定具體的型別,如 string
,這樣 TypeScript 編譯器就可以對傳入參數進行型別檢查。泛型使得這個函式可以為任何型別的參數提供相同的功能,同時保留型別安全。
泛型跟任意型別有什麼區別?
當我還沒完全理解什麼是泛型時,心裡不禁有這樣的疑問,一樣請 ChatGPT 解釋給我聽:
泛型(Generics)和任意型別(Any)在 TypeScript 中都可以用來表示一個可以容納不同型別的容器,但它們的使用場景和目的有很大的不同。
泛型是一種在程式設計時保持型別安全的方式,讓開發者可以為函式、類別或介面創建靈活且可重用的組件。泛型允許你在定義組件時保留一個或多個未指定的型別,然後在使用該組件時,由開發者明確地指定這些型別。在整個過程中,TypeScript 編譯器會追蹤這些型別,並確保它們在使用時是型別安全的。
任意型別(Any)則完全不同。當你使用任意型別時,你告訴 TypeScript 編譯器「我不在乎這個變數的型別,讓我自由地使用它,不要給我任何型別型檢查的限制。」換句話說,任意型別會讓 TypeScript 的型別系統失去效力,因為它允許你在不考慮型別安全的情況下進行操作。
總之,泛型和任意型別的主要區別在於:
泛型保持了型別安全,而任意型別則不考慮型別安全。
泛型讓你在定義組件時保留未指定的型別,然後在使用時明確指定這些型別;而任意型別完全不在乎變數的型別。
在實際開發中,應儘量避免使用任意型別,以保持型別安全和更好的程式碼可讀性。
多個型別引數
我們可以定義多個泛型型別,可以在尖括號中用逗號分隔它們,如下所示:
function swapKeyValue<T, U>(keyValuePair: [T, U]): [U, T] {
return [keyValuePair[1], keyValuePair[0]];
}
const swappedPair = swapKeyValue<number, string>([1, "apple"]);
泛型約束
有時我們會需要限制泛型的可以接受的型別範圍,避免因為傳入某些值而發生非預期的錯誤。這時我們可以使用 extends
關鍵字來對泛型進行約束:
// 若沒有對泛型變數的型別進行約束
function logLength<T>(arg: T): T {
console.log(arg.length); // 🔴 錯誤:類型 'T' 沒有屬性 'length'。
return arg;
}
interface HasLength {
length: number;
}
// 用 extends 對泛型進行約束,只允許傳入包含 length 屬性的變數
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
呼叫函式時若是傳入的引數不包含 length
屬性,編譯時也會報錯:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength(7); // 🔴 錯誤:類型 'number' 的引數不可指派給類型 'HasLength' 的參數。
泛型介面
泛型也可以用於介面,讓我們定義具有泛型型別的介面:
interface KeyValuePair<T, U> {
key: T;
value: U;
}
let pair: KeyValuePair<number, string> = { key: 1, value: "apple" };
泛型類別
泛型也可以用於類別,讓我們創建靈活且可重用的類別:
class Queue<T> {
private data: T[] = [];
push(item: T) {
this.data.push(item);
}
pop(): T | undefined {
return this.data.shift();
}
}
const queue = new Queue<number>();
queue.push(1);
queue.push(2);
console.log(queue.pop()); // 1
泛型引數的預設型別
在 TypeScript 2.3 後,我們可以為泛型中的型別引數指定預設型別。如果在使用泛型時沒有指定型別,將會使用預設型別。用 =
來指定預設型別:
// 將泛型引數的預設型別指定為 string
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
// 沒有像這樣 createArray<string>(3, "default") 指定泛型引數的型別
const defaultArray = createArray(3, "default"); // ['default', 'default', 'default']
const numberArray = createArray<number>(3, 0); // [0, 0, 0]
什麼是宣告合併(Declaration Merging)?
宣告合併是 TypeScript 的一個特性,它允許在多個地方定義相同名稱的宣告,然後 TypeScript 會將這些宣告合併成一個單一的宣告。這對於擴展現有的宣告或擴展庫非常有用。宣告合併可以應用於函式、介面或類別。
函式合併
函式不能直接進行宣告合併。但是,可以使用函式多載來達到類似的效果:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if (typeof a === 'string' && typeof b === 'string') {
return a.concat(b);
}
}
console.log(add(1, 2)); // 3
console.log(add('Hello', ' world!')); // "Hello world!"
介面合併
當多個介面具有相同的名稱時,它們將被合併成一個介面。以下是一個介面合併的範例:
interface Box {
height: number;
width: number;
}
interface Box {
depth: number;
}
const myBox: Box = {
height: 5,
width: 6,
depth: 7
};
有相同名稱的屬性時將會合併,兩者的型別需一致:
interface Info {
name: string;
age: number;
}
interface Info {
email: string;
age: string; // 這裡會報錯,因為先前的宣告中 age 是 number
}
類別合併
類別不能直接進行宣告合併,但是可以使用混入(Mixin)來達到類似的效果。以下是一個混入範例:
// 定義一個混入函式,將來源類別的屬性和方法複製到目標類別
function applyMixin(targetClass: any, sourceClass: any): void {
Object.getOwnPropertyNames(sourceClass.prototype).forEach(name => {
targetClass.prototype[name] = sourceClass.prototype[name];
});
}
class Flyable {
fly(): void {
console.log('I can fly!');
}
}
class Swimmable {
swim(): void {
console.log('I can swim!');
}
}
class Animal {}
// 將 Flyable 和 Swimmable 的屬性和方法添加到 Animal 類別
applyMixin(Animal, Flyable);
applyMixin(Animal, Swimmable);
const animal = new Animal();
(animal as any).fly(); // "I can fly!"
(animal as any).swim(); // "I can swim!"
什麼是混入(Mixin)?
混入是一種在多個類別之間共享行為的方法。透過混入,我們可以將多個類別的功能組合在一個類別中,而不需要使用繼承。混入通常用於將一些可重複使用的功能抽取出來,然後將這些功能注入到其他類別中。
// 定義一個可被混入的類別
class Runnable {
running = false;
run() {
this.running = true;
console.log("Running...");
}
stop() {
this.running = false;
console.log("Stopped.");
}
}
// 定義一個應用混入的類別
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
// 這是一個將混入類別合併到目標類別的函數
function applyMixin(targetClass: any, mixinClass: any) {
Object.getOwnPropertyNames(mixinClass.prototype).forEach(name => {
if (name !== "constructor") {
targetClass.prototype[name] = mixinClass.prototype[name];
}
});
}
// 應用混入
applyMixin(Animal, Runnable);
// 測試
const dog = new Animal("Buddy");
(dog as any).run(); // Running...
(dog as any).stop(); // Stopped.
在這個例子中,我們定義了一個 Runnable
混入類別,它包含了 run
和 stop
方法。然後,我們使用 applyMixin
函數將 Runnable
的方法注入到 Animal
類別中。這樣,Animal
類別的實例就可以使用 run
和 stop
方法了。