跳至主要內容
Skip to content

TypeScript 映射類型與索引類型

映射類型可以將一個類型轉換成另一個類型。本篇將介紹這個強大的類型轉換技巧。


一、 索引存取類型

1.1 基本用法

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

// 取得單個屬性類型
type UserId = User["id"]; // number
type UserName = User["name"]; // string

1.2 聯合索引

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

// 取得多個屬性的聯合類型
type UserIdOrName = User["id" | "name"]; // number | string

1.3 取得所有值類型

typescript
interface User {
  id: number;
  name: string;
  isAdmin: boolean;
}

type UserValues = User[keyof User]; // number | string | boolean

1.4 陣列元素類型

typescript
const users = [
  { id: 1, name: "John" },
  { id: 2, name: "Jane" },
];

type User = (typeof users)[number];
// { id: number, name: string }

二、 映射類型基礎

2.1 基本語法

typescript
// 語法:{ [K in Keys]: Type }

type Keys = "a" | "b" | "c";

type MyObject = {
  [K in Keys]: string;
};
// { a: string, b: string, c: string }

2.2 搭配 keyof

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

// 將所有屬性變成 boolean
type UserFlags = {
  [K in keyof User]: boolean;
};
// { id: boolean, name: boolean, email: boolean }

2.3 保留原始類型

typescript
interface User {
  id: number;
  name: string;
}

// 映射但保留原類型
type UserCopy = {
  [K in keyof User]: User[K];
};
// { id: number, name: string }

三、 修飾符

3.1 readonly 修飾符

typescript
interface User {
  id: number;
  name: string;
}

// 添加 readonly
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};
// { readonly id: number, readonly name: string }

// 移除 readonly(使用 -)
type MutableUser = {
  -readonly [K in keyof User]: User[K];
};

3.2 可選修飾符

typescript
interface User {
  id: number;
  name: string;
}

// 添加可選
type PartialUser = {
  [K in keyof User]?: User[K];
};
// { id?: number, name?: string }

// 移除可選
type RequiredUser = {
  [K in keyof User]-?: User[K];
};

四、 內建工具類型

4.1 Partial<T>

typescript
// 將所有屬性變成可選
type Partial<T> = {
  [K in keyof T]?: T[K];
};

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

type PartialUser = Partial<User>;
// { id?: number, name?: string }

4.2 Required<T>

typescript
// 將所有屬性變成必填
type Required<T> = {
  [K in keyof T]-?: T[K];
};

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

type RequiredUser = Required<User>;
// { id: number, name: string }

4.3 Readonly<T>

typescript
// 將所有屬性變成唯讀
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

4.4 Record<K, V>

typescript
// 建立物件類型
type Record<K extends keyof any, V> = {
  [P in K]: V;
};

type Roles = "admin" | "user" | "guest";
type RolePermissions = Record<Roles, boolean>;
// { admin: boolean, user: boolean, guest: boolean }

4.5 Pick<T, K>

typescript
// 選取特定屬性
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

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

type UserBasic = Pick<User, "id" | "name">;
// { id: number, name: string }

4.6 Omit<T, K>

typescript
// 排除特定屬性
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface User {
  id: number;
  name: string;
  password: string;
}

type PublicUser = Omit<User, "password">;
// { id: number, name: string }

五、 鍵重映射

5.1 as 子句

TypeScript 4.1+ 支援使用 as 重映射鍵:

typescript
interface User {
  id: number;
  name: string;
}

// 添加前綴
type GetterUser = {
  [K in keyof User as `get${Capitalize<string & K>}`]: () => User[K];
};
// { getId: () => number, getName: () => string }

5.2 過濾鍵

typescript
interface User {
  id: number;
  name: string;
  isAdmin: boolean;
}

// 只保留 string 類型的屬性
type StringKeysOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type StringProps = StringKeysOnly<User>;
// { name: string }

5.3 移除特定屬性

typescript
// 移除 id 屬性
type RemoveId<T> = {
  [K in keyof T as Exclude<K, "id">]: T[K];
};

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

type UserWithoutId = RemoveId<User>;
// { name: string }

六、 實用範例

6.1 Getters

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

type PersonGetters = Getters<Person>;
// { getName: () => string, getAge: () => number }

6.2 事件處理器

typescript
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};

interface Form {
  username: string;
  password: string;
}

type FormHandlers = EventHandlers<Form>;
// { onUsernameChange: (value: string) => void, onPasswordChange: (value: string) => void }

6.3 深度 Readonly

typescript
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  api: {
    url: string;
    timeout: number;
  };
}

type ReadonlyConfig = DeepReadonly<Config>;
// 所有層級都是 readonly

總結

語法說明範例
T[K]索引存取User['name']
T[keyof T]所有值類型聯合類型
{ [K in Keys]: T }映射類型遍歷鍵
readonly唯讀修飾符不可變
? / -?可選修飾符添加/移除
as鍵重映射轉換鍵名

**應用場景**:

  • 表單 state 和 errors 對應
  • API 請求和回應轉換
  • 設定物件處理

進階挑戰

  1. 實作 DeepPartial<T>:深度可選
  2. 實作 Mutable<T>:移除所有 readonly
  3. 實作一個 Path<T> 類型,生成物件的所有路徑
typescript
// 練習 3 提示
interface User {
  name: string;
  address: { city: string };
}
type P = Path<User>; // 'name' | 'address' | 'address.city'

延伸閱讀與資源