前一篇介紹了泛型的基本用法與核心概念。本文將深入探討泛型在進階應用中的強大能力,包括預設泛型型別、泛型約束、工具型別等,展示如何利用泛型來處理更複雜的型別操作。
當你希望使用泛型時,有預設的型別,來增加程式的使用便利性,可以在泛型後面用 =
操作符來賦予型別給這個泛型。
class DataStorage<T = string> {
private data: T[] = [];
addItem(item: T): void {
this.data.push(item);
}
getItems(): T[] {
return this.data;
}
}
const textStorage = new DataStorage(); // 預設型別為 string
textStorage.addItem('Hello');
console.log(textStorage.getItems()); // ['Hello']
const numberStorage = new DataStorage<number>();
numberStorage.addItem(0);
console.log(numberStorage.getItems()); // [0]
這裡的 DataStorage
類別提供了一個預設泛型型別 string,如果在實例化時未指定型別,則會使用預設型別,這提升了使用的方便性。當然,你也可以在實例化時,指定具體的泛型型別。
當這個例子如果沒有預設型別時,會因為在我們實例化類別時,TypeScript 沒辦法根據實例化的當下自動推斷這個泛型的型別,此時這個泛型就會是 unknown
。
所以通常不預設泛型,就需要強迫使用這個類別的人,具體指定一個型別。
泛型約束允許你對泛型參數施加一定的限制,讓泛型變得更加精確可控。
在某些情況下,我們不僅需要泛型能接受多種型別,還希望限制泛型需要符合某些特定條件,可以使用 extends
來約束泛型的型別。
keyof 關鍵字能讓我們獲取型別的屬性,搭配泛型約束,可以讓參數限制到只能選該型別的屬性。
function getProperty<
T extends Record<string, unknown>,
K extends keyof T
>(obj: T, key: K) {
return obj[key];
}
const person = { name: 'Johnson Mao', age: 18 };
constage name = getProperty(person, 'name'));
// name 是 string 型別
在這裡,K 被限制為 T 物件的屬性鍵,保證在使用時,只能存取合法的屬性。這種模式經常用於需要動態存取物件屬性的函式中。
你可以將泛型參數限制為實現特定屬性或方法的型別,例如:要求傳入的參數具有 length
屬性
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(item: T): void {
console.log(item.length);
}
logLength([1, 2, 3]); // 3
logLength('Hello'); // 5
// logLength(10); // Error: 類型 'number' 的引數不可指派給類型 'Lengthwise' 的參數。
這種泛型約束在處理陣列、字串等具備 length 屬性的型別時非常有用。
在前面幾篇文章中,有介紹了 Record
與 Array
,這些利用泛型來回傳型別的工具,
在實際開發中,還有很多好用的內建工具型別,這些工具型別利用了泛型的特性來簡化常見的型別操作。
將傳入的型別第一層屬性都改成可選的狀態,需注意嵌套的深層物件並不會變動唷!
type PersonType = {
name: string;
box: {
height: number;
width: number;
};
};
/**
* Partial<PersonType> 回傳的型別等同於
*
* type PersonType = {
* name?: string;
* box?: {
* height: number;
* width: number;
* };
* };
*/
function updateObject<T>(obj: T, partialObj: Partial<T>): T {
return { ...obj, ...partialObj };
}
const person: PersonType = {
name: 'Johnson Mao',
box: {
height: 100,
width: 100,
},
};
updatePerson(person, { name: 'Mao' }); // 這樣就可以傳想修改的屬性了
是 Partial 的反向操作,適合用在需要確保物件完整性的場景,將傳入的型別第一層屬性都改成必有的狀態,同樣的需注意嵌套的深層物件並不會變動唷!
type PersonType = {
name?: string;
box?: {
height?: number;
width?: number;
};
};
const person: Required<PersonType> = {
name: 'John',
box: {} // Required 只有第一層會改成必有的狀態,嵌套的則保持原狀
};
將傳入的型別第一層屬性設為唯讀,防止資料被意外修改,適合不可變資料結構的場景,同樣的需注意嵌套的深層物件並不會變動唷!
interface Ironman {
title: string;
posts: { title: string, content: string }[]
}
const ironman: Readonly<Ironman> = {
title: 'TypeScript 完全指南:從語法基礎到高級功能的系統學習',
posts: []
}
// 因為 'title' 為唯讀屬性,所以無法指派至 'title'。
// ironman.title = '我想變更標題';
// 嵌套的物件屬性可以修改
ironman.posts[0] = {
title: 'TypeScript 進階:靈活應用泛型',
content: ''
}