TypeScript 映射型別 (Mapped Types)

映射型別 (Mapped Types) 允許我們基於現有型別建立新型別,透過迭代物件型別的鍵來轉換每個屬性。這是 TypeScript 型別系統中非常強大的功能。

基本語法

type MappedType<T> = {
  [K in keyof T]: NewType;
};
  • keyof T:取得 T 的所有鍵
  • K in keyof T:迭代所有鍵
  • NewType:新的屬性型別

基本範例

將所有屬性變成可選

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

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

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

將所有屬性變成唯讀

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

interface Point {
  x: number;
  y: number;
}

type ReadonlyPoint = MyReadonly<Point>;
// { readonly x: number; readonly y: number }

將所有屬性變成特定型別

type Stringify<T> = {
  [K in keyof T]: string;
};

interface Config {
  port: number;
  host: string;
  ssl: boolean;
}

type StringConfig = Stringify<Config>;
// { port: string; host: string; ssl: string }

屬性修飾子

新增修飾子

// 新增 readonly
type AddReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// 新增 optional
type AddOptional<T> = {
  [K in keyof T]?: T[K];
};

移除修飾子

使用 - 移除修飾子:

// 移除 readonly
type RemoveReadonly<T> = {
  -readonly [K in keyof T]: T[K];
};

// 移除 optional
type RemoveOptional<T> = {
  [K in keyof T]-?: T[K];
};

// TypeScript 內建的 Required<T> 就是這樣實作的
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

鍵的重新映射 (Key Remapping)

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

// 將所有鍵改為大寫
type UppercaseKeys<T> = {
  [K in keyof T as Uppercase<K & string>]: T[K];
};

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

type UpperUser = UppercaseKeys<User>;
// { NAME: string; AGE: number }

過濾鍵

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

interface Mixed {
  name: string;
  age: number;
  email: string;
  active: boolean;
}

type StringProps = OnlyStrings<Mixed>;
// { name: string; email: string }

為鍵加上前綴

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

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

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

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

type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }

條件映射

// 將函式型別包裝成 Promise
type Promisify<T> = {
  [K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<R> : T[K];
};

interface SyncApi {
  getUser(id: number): User;
  saveUser(user: User): void;
  version: string;
}

type AsyncApi = Promisify<SyncApi>;
// {
//     getUser(id: number): Promise<User>;
//     saveUser(user: User): Promise<void>;
//     version: string;
// }

實用映射型別

Nullable<T>

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

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

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

Mutable<T>

type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
}

type MutableUser = Mutable<ReadonlyUser>;
// { id: number; name: string }

DeepPartial<T>

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

interface NestedConfig {
  server: {
    host: string;
    port: number;
  };
  logging: {
    level: string;
    format: string;
  };
}

type PartialConfig = DeepPartial<NestedConfig>;
// 所有巢狀屬性都變成可選

DeepReadonly<T>

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

映射型別與聯合型別

type EventHandlers<Events extends string> = {
  [K in Events as `on${Capitalize<K>}`]: (event: K) => void;
};

type MouseEvents = 'click' | 'mouseenter' | 'mouseleave';

type MouseEventHandlers = EventHandlers<MouseEvents>;
// {
//     onClick: (event: "click") => void;
//     onMouseenter: (event: "mouseenter") => void;
//     onMouseleave: (event: "mouseleave") => void;
// }

進階範例

物件方法轉換

type MethodsOnly<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

type PropertiesOnly<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

class User {
  name: string = '';
  age: number = 0;

  greet() {
    return `Hello, ${this.name}`;
  }

  getAge() {
    return this.age;
  }
}

type UserMethods = MethodsOnly<User>;
// { greet: () => string; getAge: () => number }

type UserProperties = PropertiesOnly<User>;
// { name: string; age: number }

建立 Form 型別

type FormFields<T> = {
  [K in keyof T]: {
    value: T[K];
    error: string | null;
    touched: boolean;
  };
};

interface LoginData {
  email: string;
  password: string;
}

type LoginForm = FormFields<LoginData>;
// {
//     email: { value: string; error: string | null; touched: boolean };
//     password: { value: string; error: string | null; touched: boolean };
// }

API 端點型別

type ApiEndpoints<T> = {
  [K in keyof T as `fetch${Capitalize<K & string>}`]: () => Promise<T[K]>;
} & {
  [K in keyof T as `update${Capitalize<K & string>}`]: (data: Partial<T[K]>) => Promise<T[K]>;
} & {
  [K in keyof T as `delete${Capitalize<K & string>}`]: (id: number) => Promise<void>;
};

interface Resources {
  user: User;
  post: Post;
}

type ResourceApi = ApiEndpoints<Resources>;
// {
//     fetchUser: () => Promise<User>;
//     updateUser: (data: Partial<User>) => Promise<User>;
//     deleteUser: (id: number) => Promise<void>;
//     fetchPost: () => Promise<Post>;
//     updatePost: (data: Partial<Post>) => Promise<Post>;
//     deletePost: (id: number) => Promise<void>;
// }

狀態與動作

type Actions<State> = {
  [K in keyof State as `set${Capitalize<K & string>}`]: (value: State[K]) => void;
} & {
  [K in keyof State as `reset${Capitalize<K & string>}`]: () => void;
};

interface AppState {
  count: number;
  user: User | null;
  theme: 'light' | 'dark';
}

type AppActions = Actions<AppState>;
// {
//     setCount: (value: number) => void;
//     resetCount: () => void;
//     setUser: (value: User | null) => void;
//     resetUser: () => void;
//     setTheme: (value: "light" | "dark") => void;
//     resetTheme: () => void;
// }

保留原始型別資訊

// 保留可選和唯讀修飾子
type Clone<T> = {
  [K in keyof T]: T[K];
};

// 這會保留所有原始的修飾子
interface Original {
  readonly id: number;
  name?: string;
  email: string;
}

type Cloned = Clone<Original>;
// { readonly id: number; name?: string; email: string }

總結

映射型別是 TypeScript 中最強大的型別操作工具之一,它可以:

  • 基於現有型別建立新型別
  • 新增或移除屬性修飾子
  • 過濾或重新命名鍵
  • 轉換屬性型別
  • 建立複雜的型別轉換