TypeScript - 映射類型:初學者指南

你好,未來的 TypeScript 巫師們!今天,我們將踏上一段令人興奮的旅程,進入映射類型的世界。別擔心你對編程還是新手——我會成為你的友好導遊,我們會一步一步地學習。在本教程結束時,你將能夠像專業人士一樣映射類型!

TypeScript - Mapped Types

映射類型是什麼?

在我們深入之前,讓我們先了解映射類型是什麼。想像你有一盒各種巧克力,你想創造一個新盒子,裡面的每個巧克力都包著金色紙。這基本上就是 TypeScript 中的映射類型所做的——它們取一個現有的類型,並根據一套規則將其轉換為新的類型。

內置的映射類型

TypeScript 提供了一些預定義的映射類型,非常實用。讓我們一個一個地看看:

1. Partial<T>

Partial<T> 類型使 T 的所有屬性變為可選。這就像將嚴格的菜譜變為靈活的菜譜,你可以跳過一些材料。

interface Recipe {
name: string;
ingredients: string[];
cookingTime: number;
}

type FlexibleRecipe = Partial<Recipe>;

// 現在我們可以創建一個不包含所有屬性的菜譜
const quickSnack: FlexibleRecipe = {
name: "烤麵包",
// 我們可以跳過材料和方法時間
};

在這個例子中,FlexibleRecipe 允許我們創建一個不指定所有屬性的菜譜。當你只想更新對象的一部分時,這非常適合。

2. Required<T>

Required<T>Partial<T> 的相反。它使所有屬性都成為必填,即使它們在原始類型中是可選的。

interface UserProfile {
name: string;
age?: number;
email?: string;
}

type CompleteUserProfile = Required<UserProfile>;

// 現在我們必須提供所有屬性
const user: CompleteUserProfile = {
name: "艾麗絲",
age: 30,
email: "[email protected]"
};

在這裡,CompleteUserProfile 確保我們提供所有屬性,包括原始 UserProfile 中可選的 ageemail

3. Readonly<T>

Readonly<T> 類型使 T 的所有屬性為唯讀。這就像將你的類型放在玻璃櫥窗裡——你可以看,但不能碰!

interface Toy {
name: string;
price: number;
}

type CollectibleToy = Readonly<Toy>;

const actionFigure: CollectibleToy = {
name: "超級英雄",
price: 19.99
};

// 這會導致錯誤
// actionFigure.price = 29.99;

在這個例子中,一旦我們創建了 actionFigure,我們就不能修改它的屬性。這對於創建不可變對象非常適合。

4. Pick<T, K>

Pick<T, K> 通過從 T 選擇指定的屬性 K 來創建一個新類型。這就像從類型中挑選你喜歡的特性。

interface Smartphone {
brand: string;
model: string;
year: number;
color: string;
price: number;
}

type BasicPhoneInfo = Pick<Smartphone, 'brand' | 'model'>;

const myPhone: BasicPhoneInfo = {
brand: "科技品牌",
model: "X2000"
};

在這裡,BasicPhoneInfo 只包括 Smartphonebrandmodel 屬性,其他屬性都被省略。

5. Omit<T, K>

Omit<T, K>Pick<T, K> 的相反。它通過從 T 中移除指定的屬性 K 來創建一個新類型。

interface Book {
title: string;
author: string;
pages: number;
isbn: string;
}

type BookPreview = Omit<Book, 'pages' | 'isbn'>;

const preview: BookPreview = {
title: "TypeScript 冒險",
author: "編程巫師"
};

在這個例子中,BookPreview 包括 Book 的所有屬性,除了 pagesisbn

映射類型的使用範例

現在我們已經看到了內置的映射類型,讓我們看看一些實際應用這些類型的例子。

範例 1:創建表單狀態

想像你正在構建一個表單,並且你想要追踪哪些字段已被修改:

interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}

type FormTouched = { [K in keyof LoginForm]: boolean };

const touchedFields: FormTouched = {
username: true,
password: false,
rememberMe: true
};

在這裡,我們創建了一個 FormTouched 類型,它具有與 LoginForm 相同的鍵,但所有值都是布爾值,表示字段是否已被觸碰。

範例 2:API 响應包裝器

假設你有一個 API,它返回不同類型的數據,並且你想要將每個響應包裝在標準格式中:

interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
timestamp: number;
}

type UserData = { id: number; name: string; email: string };
type ProductData = { id: number; name: string; price: number };

const userResponse: ApiResponse<UserData> = {
data: { id: 1, name: "約翰·多伊", email: "[email protected]" },
status: 'success',
timestamp: Date.now()
};

const productResponse: ApiResponse<ProductData> = {
data: { id: 101, name: "筆記本電腦", price: 999.99 },
status: 'success',
timestamp: Date.now()
};

這個例子展示了如何使用泛型與映射類型來創建灵活、可重用的類型結構。

創建自定義映射類型

現在,讓我們展現一下 TypeScript 的實力,創建一些自定義映射類型!

自定義類型 1:Nullable

讓我們創建一個類型,使所有屬性可為 null:

type Nullable<T> = { [K in keyof T]: T[K] | null };

interface Person {
name: string;
age: number;
}

type NullablePerson = Nullable<Person>;

const maybePerson: NullablePerson = {
name: "簡",
age: null  // 這現在是有效的
};

我們的 Nullable<T> 類型允許任何屬性為其原始類型或 null

自定義類型 2:Freezable

讓我們創建一個類型,為對象添加一個 freeze 方法:

type Freezable<T> = T & { freeze(): Readonly<T> };

interface Config {
theme: string;
fontSize: number;
}

function makeFreezable<T>(obj: T): Freezable<T> {
return {
...obj,
freeze() {
return Object.freeze({ ...this }) as Readonly<T>;
}
};
}

const config = makeFreezable<Config>({
theme: "深色",
fontSize: 14
});

const frozenConfig = config.freeze();
// frozenConfig.theme = "亮色";  // 這會導致錯誤

這個自定義類型為任何對象添加了一個 freeze 方法,允許我們創建一個不可變版本的對象。

結論

哇,我們今天學習了很多內容!從內置的映射類型到創建自己的自定義類型,你已經看到了 TypeScript 有多强大和多灵活。映射類型就像是 TypeScript 工具包中的魔杖——它們讓你可以以非常實用的方式轉換和操縱類型。

記住,掌握映射類型的關鍵是練習。嘗試創建你自己的類型,試驗不同的組合,很快你將能夠編寫出功能齐全、優雅且類型安全的 TypeScript 代碼。

繼續編碼,持續學習,最重要的是,享受 TypeScript 的樂趣!

映射類型 描述 範例
Partial<T> 使所有屬性可選 type PartialUser = Partial<User>
Required<T> 使所有屬性必填 type RequiredUser = Required<User>
Readonly<T> 使所有屬性唯讀 type ReadonlyUser = Readonly<User>
Pick<T, K> 創建一個只有指定屬性的類型 type UserName = Pick<User, 'name'>
Omit<T, K> 創建一個不包含指定屬性的類型 type UserWithoutPassword = Omit<User, 'password'>
Record<K, T> 創建一個對象類型,鍵為 K,值為 T type StringMap = Record<string, string>
Exclude<T, U> 從 T 中排除 U 中的類型 type NumberOnly = Exclude<number | string, string>
Extract<T, U> 從 T 中提取 U 中的類型 type StringOnly = Extract<number | string, string>
NonNullable<T> 從 T 中移除 null 和 undefined type NonNullableString = NonNullable<string | null | undefined>
Parameters<T> 提取函數類型的參數類型 type Params = Parameters<(a: number, b: string) => void>
ReturnType<T> 提取函數類型的返回類型 type ReturnString = ReturnType<() => string>
InstanceType<T> 提取構造函數類型的實例類型 type DateInstance = InstanceType<typeof Date>

Credits: Image by storyset