TypeScript 進階型別操作:Indexed Access Type (T[K])
簡介
在日常開發中,我們常會遇到「從一個物件型別中取出某個屬性的型別」的需求。傳統的做法是手動寫出對應的型別,或是使用 any 失去型別安全。Indexed Access Type(索引存取型別)提供了一種直接、可維護的方式:T[K] 讓我們可以在編譯期就取得 T 中屬性 K 的型別。
這個特性不僅提升了程式碼的 可讀性 與 可重用性,同時也讓 TypeScript 的型別系統更具表達力。無論是建立 API 回傳型別、實作泛型工具函式,或是打造大型專案的型別基礎建設,T[K] 都是不可或缺的利器。
核心概念
1. 基本語法與運作原理
T[K] 的語法結構如下:
type PropertyType = T[K];
T必須是 物件型別(或類別型別)。K必須是T中的 鍵(key),可以是字串、數字或keyof T。- 結果
PropertyType為T中屬性K的 型別。
範例:取得
User介面的name型別interface User { id: number; name: string; isAdmin: boolean; } type NameType = User["name"]; // string
2. 與 keyof 的結合使用
keyof T 會產生 T 所有鍵的聯集型別。將它與 T[K] 結合,可以一次取得 所有屬性型別的聯集:
type AllValueTypes<T> = T[keyof T];
這在需要 遍歷 物件所有屬性時非常有用。
3. 索引存取的分布式條件型別(Distributive Conditional Types)
在泛型條件型別中使用 T[K],會自動分布到每個聯合成員上,讓我們能寫出更彈性的工具型別。例如:
type PropIfString<T, K extends keyof T> = T[K] extends string ? K : never;
此型別會挑出 T 中 型別為 string 的屬性鍵。
4. 讀寫限制:readonly 與可選屬性
T[K] 只會返回屬性的 值型別,不會保留 readonly 或 ?(可選)修飾子。若需要保留這些資訊,必須搭配其他工具型別(如 Pick、Partial)使用。
程式碼範例
以下提供 5 個實務範例,展示 T[K] 在不同情境下的應用。
範例 1:從 API 回傳型別中抽取特定欄位
interface ApiResponse {
status: number;
data: {
userId: string;
token: string;
expires: Date;
};
error?: string;
}
// 只想要取得 data 裡面的型別
type DataType = ApiResponse["data"]; // { userId: string; token: string; expires: Date; }
// 再抽出 token 的型別
type TokenType = ApiResponse["data"]["token"]; // string
這樣寫可以避免手動複製
data結構,當ApiResponse改變時型別會自動同步。
範例 2:建立「屬性值」的聯集型別
interface Config {
host: string;
port: number;
useSsl: boolean;
}
// 所有屬性的型別聯集
type ConfigValue = Config[keyof Config]; // string | number | boolean
在需要 驗證 或 映射 任意屬性值時,
ConfigValue可直接作為參數型別。
範例 3:從物件取出「字串屬性」的鍵
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never
}[keyof T];
interface Product {
id: number;
name: string;
description: string;
price: number;
}
// 只留下 string 型別的鍵
type ProductStringKeys = StringKeys<Product>; // "name" | "description"
這種技巧常用於 表單生成器,只挑出需要文字輸入的欄位。
範例 4:使用 T[K] 建立「映射」函式
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", active: true };
const name = pluck(user, "name"); // name 的型別為 string
const active = pluck(user, "active"); // active 的型別為 boolean
pluck函式的返回型別直接由T[K]推斷,使用者在呼叫時會得到正確的型別提示。
範例 5:搭配條件型別建立「必填屬性」的子型別
type RequiredProps<T> = {
[K in keyof T]-?: T[K];
};
type UserPartial = {
id?: number;
name?: string;
email?: string;
};
// 變成全部必填
type UserRequired = RequiredProps<UserPartial>;
// { id: number; name: string; email: string; }
雖然此例主要示範
-?(移除可選),但在實作時常會結合T[K]取得每個屬性的原始型別,以避免遺失資訊。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 使用不存在的鍵 | T["notExist"] 會直接報錯。 |
使用 K extends keyof T 限制泛型,或先 keyof T 取得合法鍵集合。 |
readonly/? 失去 |
T[K] 只回傳值型別,readonly 與 ? 會被忽略。 |
若需要保留修飾子,搭配 Pick<T, K>、Partial<T> 等工具型別。 |
| 聯合類型的分布行為 | 在條件型別中使用 T[K] 時會自動分布,可能產生意外的 never。 |
明確使用 [K] 包裹或 Extract<...> 來控制分布。 |
| 索引類型的遞迴 | 直接 type DeepValue<T> = T[keyof T] 會在深層物件上失去結構。 |
需要遞迴條件型別來保留深層結構,例如 type DeepPick<T, K extends keyof T> = { [P in K]: DeepPick<T[P], keyof T[P]> }. |
最佳實踐
限制鍵的範圍
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { ... }這樣可以保證只能傳入合法鍵,避免編譯錯誤。
結合
as const
若想要把字面量陣列的元素當作鍵使用,記得加上as const,讓 TypeScript 推斷為字面量型別:const fields = ["id", "name"] as const; type Field = typeof fields[number]; // "id" | "name"使用
keyof產生聯合鍵
在需要「遍歷」所有屬性時,keyof T搭配映射型別是最簡潔的寫法。保持型別同步
當原始介面變更時,依賴T[K]的型別會自動更新,減少手動維護的成本。
實際應用場景
API 客戶端 SDK
透過Response["data"]直接抽取資料型別,讓呼叫端只關注實際 payload,減少重複宣告。表單生成器
使用StringKeys<T>或NumberKeys<T>只挑出需要的欄位,動態產生 UI,且型別安全。資料映射函式
pluck、mapValues等通用工具函式的返回型別皆可由T[K]推斷,提升函式的可重用性。國際化 (i18n) 文字資源
定義type LocaleKey = keyof typeof zhTW;再用LocaleKey取值,保證存取的 key 必定存在。Redux / Zustand 狀態管理
從全域狀態型別中抽取子狀態(State["user"]),在 selector 中保持型別一致。
總結
- Indexed Access Type (
T[K]) 讓我們能在編譯期直接取得物件屬性的型別,提升型別安全與維護性。 - 透過
keyof、映射型別、條件型別 等組合,可實作出高度彈性的工具型別,如挑選字串屬性鍵、產生值型別聯集等。 - 在實務開發中,
T[K]常見於 API SDK、表單生成、狀態管理等情境,對大型專案的型別一致性尤其重要。 - 注意避免鍵不存在、修飾子遺失與分布式條件型別的意外行為,遵守 限制鍵範圍、結合
as const等最佳實踐,可讓程式碼更穩健。
掌握了 T[K] 之後,你就能以更精簡、可讀的方式描述複雜資料結構,讓 TypeScript 的型別系統真正發揮威力。祝你在 TypeScript 的進階型別世界裡玩得開心!