在使用 TypeScript 的過程中,我們經常會遇到需要將一個型別轉換成另一個型別的需求。
映射型別(Mapped Types)就像是陣列的 map
方法,但它們處理的對象是型別而非值。
透過映射型別,你可以輕鬆地改變型別的屬性、控制屬性的可選性、生成新屬性等。今天,我們就來深入探索映射型別的各種實用範例,以及如何在專案中靈活運用這項強大功能!
映射型別最基礎的應用就是將某個型別的所有屬性轉換成另一個型別。比方說,我們想把一個 User
型別的所有屬性都變成布林值,這樣可以快速檢查每個屬性是否有值。
type User = {
name: string;
age: number;
isActive: boolean;
};
// 將 User 型別的所有屬性轉換成布林值
type UserToBoolean = {
[Key in keyof User]: boolean;
};
// 結果型別 UserToBoolean
// {
// name: boolean;
// age: boolean;
// isActive: boolean;
// }
這樣的轉換就像是在使用 JavaScript 中的 map
方法,但這裡我們改變的是型別。使用 keyof
和映射型別,可以在 TypeScript 中進行靈活的型別轉換。這種型別轉換特別適用於需要在不同邏輯中切換資料格式的場景,例如處理表單輸入驗證結果。
有時候,我們希望型別的屬性變得可選。在 TypeScript 中,我們通常使用內建的 Partial
工具型別,但其實我們也可以用映射型別自己實現這個效果。
type Product = {
name: string;
price: number;
inStock: boolean;
};
// 將 Product 的所有屬性變成可選
type ProductToOptional = {
[Key in keyof Product]?: Product[Key];
};
// 結果型別 ProductToOptional
// {
// name?: string;
// price?: number;
// inStock?: boolean;
// }
這樣的型別讓我們可以靈活地決定哪些屬性是可選的,這在處理表單輸入或是 API 更新資料時特別有用。比如在 PATCH 請求中,只更新需要變更的字段,其餘字段保持不變。
有時候,我們想確保某些物件的屬性不會在程式中被意外修改。這時候,我們可以利用映射型別來將屬性設定為唯讀。
type Product = {
name: string;
price: number;
inStock: boolean;
};
// 將 Product 的所有屬性設為唯讀
type ProductToReadonly = {
readonly [Key in keyof Product]: Product[Key];
};
// 結果型別 ProductToReadonly
// {
// readonly name: string;
// readonly price: string;
// readonly inStock: boolean;
// }
這樣的型別就能確保物件一旦被建立後,它的屬性就無法再被修改,有助於提升程式的穩定性。這在防止 API 回應資料被意外篡改時非常有用,確保資料一旦從 API 接收到就不會再被誤操作修改。
有時候,我們只想要型別的部分屬性,或者想移除某些不需要的屬性。Omit
是內建的工具型別之一,能達到這樣的效果,但我們也可以用映射型別來自定義這個邏輯。
type Product = {
name: string;
price: number;
inStock: boolean;
};
// 使用映射型別移除 'price' 屬性
type ProductWithoutPrice = {
[Key in keyof Product as Key extends 'price' ? never : Key]: Product[Key];
};
// 結果型別 ProductWithoutPrice
// {
// name: string;
// inStock: boolean;
// }
在這裡,我們利用了 as
和條件型別來篩選屬性,只保留我們想要的部分。這在需要對物件進行精確控制時非常實用,比如在傳送資料給前端時,避免暴露不必要的資料。
TypeScript 還允許我們使用模板字面型別(Template Literal Types)來創建新屬性名稱。這可以幫助我們生成一組有規律的屬性,例如加上 get
前綴的屬性。
type Product = {
name: string;
price: number;
inStock: boolean;
};
// 使用模板字面型別生成具有 get 前綴的屬性
type Getters<Type> = {
[Key in keyof Type as `get${Capitalize<string & Key>}`]: () => Type[Key];
};
// 結果型別 ProductGetters
type ProductGetters = Getters<Product>;
// {
// getName: () => string;
// getPrice: () => number;
// getInStock: () => boolean;
// }
這樣,我們就能自動生成一個包含多個 getter
方法的型別,讓程式碼變得更為一致且易於擴充。這在大型專案中能大幅減少重複的手動定義,提升開發效率。
在處理嵌套的物件型別時,有時我們希望將內部的每個屬性都轉變為唯讀屬性。這時候,我們可以結合條件型別和映射型別,創建深度的型別轉換。
type NestedObject = {
id: number;
name: string;
metadata: {
createdAt: Date;
updatedAt: Date;
tags?: string[];
};
tags: string[];
};
// 創建 DeepReadonly 型別,將所有屬性變成唯讀
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// 結果型別 ReadonlyNestedObject
type ReadonlyNestedObject = DeepReadonly<NestedObject>;
這樣的深度轉換能確保我們在操作複雜的嵌套物件時,保持型別的一致性,並避免資料被不小心更改。特別是在管理 API 的回應資料時,這樣的轉換可以避免資料不小心被修改。
null
和 undefined
的挑戰 🚫在實際專案中,我們經常遇到包含 null
或 undefined
值的物件。這些值在映射型別中也需要被謹慎處理。以下是一個可以將 null
和 undefined
過濾掉的範例:
type RemoveNullAndUndefined<T> = {
[Key in keyof T]: Exclude<T[Key], null | undefined>;
};
type User = {
name: string | null;
age?: number;
};
// 結果型別 UserWithoutNull
type UserWithoutNull = RemoveNullAndUndefined<User>;
// {
// name: string;
// age: number;
// }
這樣的處理能確保在使用物件時,減少因為 null
或 undefined
引發的錯誤,讓程式更穩定。
,適應不同的業務需求。
3. 過濾屬性與高階轉換:透過條件型別和模板字面型別,實現高階的屬性過濾與自定義屬性名稱。
4. 深度轉換:結合映射型別與條件型別,實現深層次的型別轉換,適用於處理複雜的嵌套結構。
5. 處理 null
和 undefined
:有效過濾不必要的空值,提升型別安全性。
映射型別為 TypeScript 帶來了更多的可能性,讓我們能夠更加靈活地操控型別結構。熟練掌握這些技巧,不僅能讓你的程式碼更加整潔有序,還能提升可維護性和可讀性,讓開發變得更有樂趣!💡