TypeScript 中的高級類型包括映射類型、條件類型、字面量類型和遞歸類型等強大結構。這些特性使開發者能夠表達類型之間更復雜的關系,從而處理邊緣情況,并定義更動態、更靈活的類型系統。
一、映射類型
TypeScript 映射類型(Mapped Types)是一種高級類型工具,它允許我們基于已有的類型創建新的類型。通過遍歷已有類型的鍵(key),并對其進行變換,可以快速構造具有相同結構但屬性類型不同的新類型,從而提高代碼的靈活性和復用性。
(一) 概念
TypeScript 中的映射類型(Mapped Types)允許你通過轉換現有類型的屬性來創建新的類型。
- 它們支持對屬性進行修改,例如將屬性設為可選(optional)、只讀(read-only),或者改變屬性的類型。
- 映射類型有助于減少重復代碼,并通過自動化的類型轉換提升類型安全性。
- 它們特別適用于創建現有類型的不同變體,而無需手動重新定義每個屬性。
例如下面的代碼所示:
type User = {id: number;name: string;email: string;
};type PartialUser = {[P in keyof User]?: User[P];
};
代碼解釋:
PartialUser
是一個新類型,其中User
類型的每個屬性都被標記為可選(optional)。keyof
操作符用于獲取User
類型的所有屬性鍵,而映射類型則會遍歷每一個鍵,并通過?
將它們標記為可選屬性。
輸出:
const user1: PartialUser = { id: 1 };
const user2: PartialUser = {};
const user3: PartialUser = { id: 2, name: "Alice" };
(二) 場景示例
1. 創建只讀屬性
在 TypeScript 中,可以使用 映射類型(Mapped Types) 和 readonly
關鍵字創建一個新的類型,使其所有屬性變為只讀。
type User = {id: number;name: string;email: string;
};type ReadonlyUser = {readonly [P in keyof User]: User[P];
};const user: ReadonlyUser = { id: 1, name: "Alice", email: "alice@example.com" };
user.id = 2;
代碼解釋:
- ReadonlyUser 是一個新類型,其中 User 的所有屬性都被標記為只讀(readonly)。
- 嘗試修改 user 對象的任何屬性都會導致編譯時錯誤。
輸出:
Error: Cannot assign to 'id' because it is a read-only property.
2. 創建可為空屬性
創建可為空屬性(Creating Nullable Properties)指的是將一個類型中的所有屬性變為可以為 null
的類型。在 TypeScript 中,可以使用映射類型(Mapped Types)結合聯合類型(Union Types)來實現這一點。
type Product = {name: string;price: number;inStock: boolean;
};type NullableProduct = {[P in keyof Product]: Product[P] | null;
};const product: NullableProduct = { name: "Laptop", price: null, inStock: true };
代碼解釋:
- NullableProduct 是一個新的類型,它使得 Product 類型中的每個屬性都可以是其原始類型,或者是
null
。 - 這允許屬性顯式地具有
null
值,用于表示該屬性當前沒有值或值缺失的情況。
輸出:
{ name: "Laptop", price: null, inStock: true }
3. 使用模板字面量重命名屬性
使用模板字面量重命名屬性(Renaming Properties with Template Literals)是 TypeScript 中映射類型的一種高級用法。它允許我們通過字符串模板語法在創建新類型時動態地改變屬性名。
type Person = {firstName: string;lastName: string;
};type PrefixedPerson = {[P in keyof Person as `person${Capitalize<P>}`]: Person[P];
};const person: PrefixedPerson = { personFirstName: "Felix", personLastName: "Raink" };
代碼解釋:
- PrefixedPerson 創建了一個新類型,通過在 Person 類型的每個屬性名前加上 "person" 前綴,并將原屬性名首字母大寫。
- 這個示例演示了如何結合模板字面量類型和 TypeScript 內置的 Capitalize 工具類型來轉換屬性名稱。
輸出:
{ personFirstName: "Felix", personLastName: "Raink" }
(三) TypeScript 映射類型使用最佳實踐
- 保持轉換簡單:避免過于復雜的嵌套轉換,以保持代碼的可讀性和維護的便捷性。
- 確保類型安全:利用映射類型強制執行一致的屬性轉換,提高整個代碼庫的類型安全性。
- 結合內置工具類型使用:配合 Partial、Readonly、Pick 和 Omit 等內置工具類型,簡化常見的類型轉換操作。
二、條件類型
在 TypeScript 中,條件類型使開發者能夠根據條件創建類型,從而實現更動態和靈活的類型定義。
它們遵循語法 T extends U ? X : Y
,意思是如果類型 T
可賦值給類型 U
,則類型解析為 X
;否則解析為 Y
。
(一) 概念
條件類型在創建工具類型和進行高級類型操作時特別有用,能夠增強代碼的復用性和類型安全性。
比如下面這個例子:
type IsString<T> = T extends string ? 'Yes' : 'No';type Test1 = IsString<string>;
type Test2 = IsString<number>;console.log('Test1:', 'Yes');
console.log('Test2:', 'No');
- 類型別名
IsString
使用條件類型來判斷類型T
是否繼承自string
。 - 如果
T
可以賦值給string
,則結果為'Yes'
;否則結果為'No'
。 Test1
被評估為'Yes'
,因為string
繼承自string
。Test2
被評估為'No'
,因為number
不繼承自string
。
輸出:
Test1: Yes
Test2: No
(二) 場景示例
1. 條件類型約束
條件類型約束允許在條件類型中對泛型類型進行約束,從而實現動態且精確的類型處理。
type CheckNum<T> = T extends number ? T : never;type NumbersOnly<T extends any[]> = {[K in keyof T]: CheckNum<T[K]>;
};const num: NumbersOnly<[4, 5, 6, 8]> = [4, 5, 6, 8];
const invalid: NumbersOnly<[4, 6, "7"]> = [4, 6, "7"];
代碼解釋:
CheckNum<T>
確保只保留數字類型;其他類型則解析為never
。NumbersOnly<T>
對數組中的每個元素應用CheckNum
,過濾掉非數字類型。
輸出:
Type '"7"' is not assignable to type 'never'.
2. 條件類型中的類型推斷
此特性允許在條件類型定義中提取并使用類型,從而實現精確的類型轉換。
type ElementType<T> = T extends (infer U)[] ? U : never;const numbers: number[] = [1, 2, 3];
const element: ElementType<typeof numbers> = numbers[0];
const invalidElement: ElementType<typeof numbers> = "string";
代碼解釋:
- ElementType<T> 使用 infer 關鍵字從數組中提取元素類型。
- 變量 element 被正確推斷為 number;嘗試賦值為字符串則無效。
輸出:
Type 'string' is not assignable to type 'number'.
3. 分布式條件類型
分布式條件類型(Distributive Conditional Types)是 TypeScript 條件類型的一種特性,當條件類型作用于聯合類型時,會將條件應用到聯合類型的每個成員上,然后將結果合并成新的聯合類型。
type Colors = 'red' | 'blue' | 'green';type ColorClassMap = {red: 'danger';blue: 'primary';green: 'success';
};type MapColorsToClasses<T extends string> = T extends keyof ColorClassMap? { [K in T]: ColorClassMap[T] }: never;const redClass: MapColorsToClasses<Colors> = { red: 'danger' };
const invalidClass: MapColorsToClasses<Colors> = { yellow: 'warning' };
代碼解釋:
MapColorsToClasses<T>
會檢查類型T
是否匹配ColorClassMap
中的某個鍵,并將其映射為對應的值。- 像
'yellow'
這樣無效的顏色會被拒絕,因為它不在ColorClassMap
中定義。
輸出:
Type '{ yellow: "warning"; }' is not assignable to type 'never'.
(三) TypeScript 條件類型的最佳實踐
- 使用條件類型創建靈活且可復用的類型定義。
- 將條件類型與泛型結合使用,以增強適應性。
- 在復雜場景中利用
infer
關鍵字實現類型推斷。
三、字面量類型
TypeScript 的字面量類型允許開發者為變量、函數參數或屬性指定精確的值,通過確保變量只能持有預定義的值來增強類型安全性。
- 允許變量具有特定且精確的值。
- 通過限制允許的值范圍,提高代碼的可靠性。
以下是 TypeScript 中字面量類型的幾種類型:
(一) 字符串字面量類型
字符串字面量類型允許變量只接受特定的一組字符串值。
type Direction = "Up" | "Down" | "Left" | "Right";let move: Direction;move = "Up"; // 有效賦值
// move = "Forward"; // 錯誤:類型 '"Forward"' 不能賦值給類型 'Direction'
- Direction 類型只能是指定的字符串字面量之一:“Up”、“Down”、“Left”或“Right”。
- 賦值為該集合之外的任何值都會導致編譯時錯誤。
(二) 數字字面量類型
數字字面量類型限制變量只能取特定的一組數值。
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;function rollDice(): DiceRoll {return 4; // 有效的返回值// return 7; // 錯誤:類型 '7' 不能賦值給類型 'DiceRoll'
}
- DiceRoll 類型只能是 1 到 6 之間的數字之一。
- 返回任何不在該范圍內的值都會導致編譯錯誤。
(三) 布爾字面量類型
布爾字面量類型限制變量只能是布爾值 true 或 false。
type Success = true;function operation(): Success {return true; // 合法的返回值// return false; // 錯誤:類型 'false' 不能賦值給類型 'true'
}
- Success 類型嚴格限定為 true,返回 false 會導致編譯時錯誤。
(四) TypeScript 字面量類型的最佳實踐
- 使用字面量類型指定精確值:定義變量時使用字面量類型,將其限制為特定的預設值,從而提升代碼的可預測性。
- 結合聯合類型使用:利用聯合類型讓變量能接受有限的多個字面量值,增強類型安全性。
- 利用類型別名:為復雜的字面量類型組合創建類型別名,簡化代碼結構并提升可讀性。
四、模板字面量類型
TypeScript 中的模板字面量類型允許通過使用模板字面量語法,將已有的字符串字面量類型組合,構造出新的字符串字面量類型。
(一) 概念
它們支持通過在模板字符串中嵌入聯合類型或其他字面量類型,創建復雜的字符串模式。
該特性提升了類型安全性,使開發者可以在類型層面定義并強制執行特定的字符串格式。
示例代碼:
type Size = "small" | "medium" | "large";
type SizeMessage = `The selected size is ${Size}.`;let message: SizeMessage;message = "The selected size is small."; // 有效
message = "The selected size is extra-large."; // 報錯
代碼解釋:
Size
是一個聯合類型,表示可能的尺寸值。SizeMessage
是一個模板字面量類型,通過嵌入Size
,構造出具體的字符串模式。- 變量
message
只能被賦值為符合SizeMessage
模式的字符串。
錯誤信息示例:
Type '"The selected size is extra-large."' is not assignable to type 'SizeMessage'.
(二) 場景示例
1. 使用 TypeScript 字面量定義路徑
type ApiEndpoints = "users" | "posts" | "comments";
type ApiPath = `/api/${ApiEndpoints}`;const userPath: ApiPath = "/api/users";
const invalidPath: ApiPath = "/api/unknown";
ApiEndpoints
是一個聯合類型,表示可能的 API 端點名稱。ApiPath
是一個模板字面量類型,動態構造出以/api/
開頭,后接ApiEndpoints
中任意值的字符串模式。userPath
是有效的,因為它符合構造的模式;而invalidPath
會報錯。
錯誤信息示例:
Type '"/api/unknown"' is not assignable to type 'ApiPath'.
2. 使用模板字面量格式化消息
type Status = "success" | "error" | "loading";
type StatusMessage = `The operation is ${Status}.`;const successMessage: StatusMessage = "The operation is success.";
const invalidMessage: StatusMessage = "The operation is pending.";
Status
是一個聯合類型,表示操作的可能狀態。StatusMessage
構造字符串模式,用于描述操作狀態。successMessage
是有效的,因為它符合模式;而invalidMessage
報錯,因為"pending"
不屬于Status
類型。
錯誤信息示例:
Type '"The operation is pending."' is not assignable to type 'StatusMessage'.
五、遞歸類型
TypeScript 為 JavaScript 添加了強類型支持。遞歸類型定義了可以自我引用的類型,適用于樹形結構或嵌套對象。實用工具類型(Utility Types)則簡化了類型的修改,比如將屬性設為可選或只讀。這些工具幫助我們寫出更清晰、更靈活的代碼。
(一) TypeScript 中的遞歸類型
遞歸類型是在其定義中引用自身的類型。這使得我們能夠建模復雜的數據結構,比如樹、鏈表和嵌套對象,其中類型可以嵌套自身。
- 允許定義自我引用的類型。
- 適合表示層級或嵌套數據。
- 必須使用 TypeScript 的類型別名(type alias)或接口(interface)來定義遞歸結構。
1. 語法示例
下面是一個表示樹結構的簡單遞歸類型:
type TreeNode = {value: number;children?: TreeNode[];
};
TreeNode
類型包含一個value
屬性和一個可選的children
屬性。children
是一個TreeNode
數組,支持嵌套結構。
2. 使用遞歸類型示例
const tree: TreeNode = {value: 1,children: [{ value: 2 },{value: 3,children: [{ value: 4 },{ value: 5 }]}]
};
在此示例中,tree
表示一個層級結構,節點可以有子節點,子節點又可以有自己的子節點,依此類推。
3. 遞歸類型的優點
- 建模復雜結構:遞歸類型方便表示層級結構。
- 類型安全:TypeScript 確保遞歸結構在每個嵌套層級都符合正確的類型。
- 類型復用:遞歸類型可以在多種場景中復用相同的結構定義。
(二) TypeScript 中的實用工具類型(Utility Types)
TypeScript 內置了一些實用工具類型,提供了修改或轉換其他類型的現成功能,簡化常見的類型操作,提高開發效率。
1. 常見實用工具類型
Partial<T>
將類型 T 的所有屬性設為可選。
適用于創建部分屬性可缺失的對象。
type Partial<T> = {[P in keyof T]?: T[P];
};
Required<T>
將類型 T 的所有屬性設為必需。
確保對象中的所有屬性必須存在。
type Required<T> = {[P in keyof T]?: T[P];
};
Readonly<T>
將類型 T 的所有屬性設為只讀。
防止初始化后修改對象屬性。
type Readonly<T> = {readonly [P in keyof T]: T[P];
};
Pick<T, K>
從類型 T 中挑選出屬性 K 的子集。
適用于從復雜類型中選取部分屬性。
type Pick<T, K extends keyof T> = {[P in K]: T[P];
};
Omit<T, K>
從類型 T 中剔除屬性 K。
用于排除不需要的屬性。
type Omit<T, K extends keyof T> = {[P in Exclude<keyof T, K>]: T[P];
};
Record<K, T>
構造一個對象類型,其屬性鍵為 K,值為 T。
用于定義類似映射的數據結構。
type Record<K extends keyof any, T> = {[P in K]: T;
};
Exclude<T, U>
從類型 T 中排除可賦值給 U 的類型。
用于過濾聯合類型中的部分類型。
type Exclude<T, U> = T extends U ? never : T;
Extract<T, U>
從類型 T 中提取可賦值給 U 的類型。
用于縮小聯合類型范圍。
type Extract<T, U> = T extends U ? T : never;
NonNullable<T>
從類型 T 中排除 null
和 undefined
。
確保值不是 null 或 undefined。
type NonNullable<T> = T extends null | undefined ? never : T;
2. 實用工具類型的優點
- 簡化類型轉換:內置的工具類型方便對類型結構進行變換(如變成可選、必需等)。
- 提升代碼可讀性:使用簡潔的類型關鍵字表達轉換意圖,使代碼更清晰。
- 提高開發效率:避免重復手寫復雜類型定義,減少錯誤。
通過遞歸類型和實用工具類型,TypeScript 為復雜數據結構和類型操作提供了強大而靈活的支持,助力開發者編寫更安全、更高效的代碼。