本文 AI 產出,尚未審核

TypeScript 泛型工具型別:keyoftypeofin

簡介

在大型前端或 Node.js 專案中,型別安全是維持程式碼品質的關鍵。TypeScript 的泛型讓我們可以寫出彈性且可重用的函式或類別,而配合 keyoftypeofin 這三個工具型別,則能把「值」與「型別」之間的關係寫得更精確、更自動化。

  • keyof 讓我們從物件型別抽取所有屬性名稱,形成字面量聯合型別。
  • typeof變數的實際值 轉換成型別,避免手動重複寫型別宣告。
  • in 則是映射型別(Mapped Types)的核心語法,能以動態方式產生新型別。

掌握這三個工具型別,能在 編譯期即捕捉錯誤、減少冗餘程式碼,提升開發效率與可維護性。以下將逐一說明概念、範例、常見陷阱與實務應用。


核心概念

1. keyof:從型別抽取鍵名

keyof T 會回傳型別 T 所有屬性的字面量聯合型別。例如:

interface User {
  id: number;
  name: string;
  email?: string;
}

// 取得 User 的所有鍵名
type UserKey = keyof User; // "id" | "name" | "email"

為什麼有用?

  • 限制函式參數:只接受物件中實際存在的鍵,防止打錯字。
  • 動態存取屬性:搭配泛型與索引存取 (obj[key]) 時,TS 能正確推斷回傳型別。
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const alice: User = { id: 1, name: "Alice" };
const email = getProp(alice, "email"); // email 型別是 string | undefined

2. typeof:把值變成型別

typeof 與 JavaScript 的 typeof 不同,它在型別層級上使用,用來取得變數或常數的型別

const API_ENDPOINT = "https://api.example.com/v1";
type Endpoint = typeof API_ENDPOINT; // string literal type: "https://api.example.com/v1"

應用情境

  • 共享常數與型別:避免手動寫 type Endpoint = string;,確保常數改名時型別同步更新。
  • 從函式返回值推斷型別
function createConfig() {
  return {
    host: "localhost",
    port: 8080,
    secure: false,
  };
}
type Config = ReturnType<typeof createConfig>; // { host: string; port: number; secure: boolean; }

3. in:映射型別(Mapped Types)

in 用在 型別映射,可以把一個鍵集合(如 keyof)映射成新型別。最常見的寫法是:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

範例:將所有屬性設為只讀

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type ReadonlyUser = Readonly<User>;
// {
//   readonly id: number;
//   readonly name: string;
//   readonly email?: string;
// }

結合 keyoftypeof 與條件型別

const permissions = {
  read: "READ",
  write: "WRITE",
  delete: "DELETE",
} as const;

type PermissionKey = keyof typeof permissions; // "read" | "write" | "delete"

type PermissionMap = {
  [K in PermissionKey]: boolean;
};

const userPermissions: PermissionMap = {
  read: true,
  write: false,
  delete: false,
};

程式碼範例彙總

以下提供 5 個實用範例,示範三個工具型別的結合使用,並附上註解說明。

// ------------------- 範例 1:安全的屬性存取 -------------------
interface Product {
  id: number;
  name: string;
  price: number;
}

// 泛型函式,只允許存取 Product 真正的鍵名
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach((k) => (result[k] = obj[k]));
  return result;
}

const p: Product = { id: 1, name: "筆記本", price: 1200 };
const mini = pick(p, ["id", "name"]); // { id: 1, name: "筆記本" }
// ---------------------------------------------------------

// ------------------- 範例 2:從常數自動產生型別 -------------------
const STATUS = {
  SUCCESS: "success",
  FAIL: "fail",
  PENDING: "pending",
} as const;

// 使用 typeof + keyof 產生合法的狀態字串型別
type Status = typeof STATUS[keyof typeof STATUS]; // "success" | "fail" | "pending"

function setStatus(s: Status) {
  console.log(`Current status: ${s}`);
}
setStatus("success"); // 正確
// setStatus("unknown"); // 編譯錯誤
// ---------------------------------------------------------

// ------------------- 範例 3:動態建立 DTO -------------------
type ApiResponse<T> = {
  data: T;
  error?: string;
};

type UserDTO = {
  userId: number;
  username: string;
};

type UserResponse = ApiResponse<UserDTO>;
// 等同於 { data: { userId: number; username: string }; error?: string; }

// ------------------- 範例 4:條件映射型別 -------------------
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email?: string | null; }

// ------------------- 範例 5:深層只讀(Recursive Readonly) -------------------
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

type Config = {
  api: {
    endpoint: string;
    timeout: number;
  };
  debug: boolean;
};

type ImmutableConfig = DeepReadonly<Config>;
// 所有層級都變成 readonly,編譯期防止意外變更

常見陷阱與最佳實踐

陷阱 說明 解決方式
**keyof 產生 `string number symbol`**
typeof 失去字面量類型 直接 typeof 變數會得到「寬鬆」的型別(如 string),除非使用 as const 斷言。 為常數加上 as const,或使用 const enum(在需要編譯期常量時)。
映射型別遺失可選屬性 (?) in 產生的新型別預設會把所有屬性變為必填。 在映射時保留可選性:[P in keyof T]-?: T[P]-? 移除可選)或 +?(保留)視需求而定。
遞迴映射型別造成「過深」錯誤 深層遞迴(如 DeepReadonly)在極端情況下會觸發 TypeScript 的遞迴深度限制。 限制遞迴深度或使用 any/unknown 作為終止條件;在 tsconfig.json 中調整 maxDepth--maxNodeModuleJsDepth 只針對模組)較少見。
使用 in 時忘記加上索引簽名 ([key: string]) 若映射的鍵集合不完整,TypeScript 仍會要求符合原型別的所有鍵。 使用 Partial<T>Record<string, T> 來彈性處理未知鍵。

最佳實踐

  1. 盡量使用 as const 讓字面量保持最精確的型別,配合 typeof 可產生「字面量型別」。
  2. 在泛型約束中加入 keyof,保證傳入的鍵一定是合法屬性,避免 any 的不安全。
  3. 利用條件型別 (T extends ... ? ... : ...) 結合 in,寫出更具彈性的工具型別(如 Partial, Required, Pick)。
  4. 保持型別與實作同步:若常數或 API 回傳結構改變,只需要更新一次(使用 typeofReturnType),所有相關型別自動更新。
  5. 在大型專案中建立共用的型別工具檔(如 type-utils.ts),集中管理 PartialDeep, Mutable, DeepPartial 等自訂型別,提升可維護性。

實際應用場景

場景 使用的工具型別 為什麼適合
表單驗證:根據 API 回傳的欄位自動產生表單型別 keyof + 映射型別 (in) 只要 API 定義變動,表單型別自動更新,減少手動同步的錯誤。
Redux / Zustand 狀態管理:從 initialState 推斷狀態型別 typeof + keyof initialState 只寫一次,型別自動推斷,避免 state 結構不一致。
多語系文字資源:從語言檔案生成合法的 key 型別 typeof + keyof + in 確保 t('some.key') 的 key 必須存在於語言檔,編譯期即捕捉錯字。
API 客戶端:根據後端 Swagger JSON 動態產生請求函式的參數型別 typeof + 映射型別 只要 Swagger 更新,型別自動重新產生,客戶端永遠與後端同步。
權限系統:從一組權限常數產生角色對應的布林映射 typeof + keyof + in 只要新增或刪除權限,所有角色的型別都會即時更新,避免遺漏檢查。

總結

keyoftypeofin 是 TypeScript 泛型工具型別 中最基礎、也最威力強大的三把鑰匙。透過:

  • keyof 抽取 屬性鍵,讓函式參數、映射型別更安全;
  • typeof 直接轉成 型別,避免重複宣告與同步問題;
  • in 建立 映射型別,可動態產生 Partial、Readonly、Deep 等高階型別,

我們能在 編譯期即捕捉錯誤、減少冗餘程式碼,並在大型專案中保持 型別與實作的一致性。只要遵守前述的最佳實踐,並留意常見陷阱,就能把 TypeScript 的型別系統發揮到最大效能,寫出更可靠、可維護的程式碼。

實務提醒:在日常開發中,先從 as const + typeof 建立「常數型別」開始,逐步擴展到 keyof + in 的映射型別,讓型別安全成為開發流程的自然一環。祝你在 TypeScript 的世界裡玩得開心、寫得乾淨!