跳至主要内容

映射類型(Mapped Types)

什麼是映射類型 (Mapped Types)

如果今天有一個類型全部都是一樣的 (例:string) 並且 key 非常非常的多,每一筆都寫 string 就會顯的又臭又長

type TInfo = {
firstname: string,
lastname: string,
sex: string,
username: string,
zip: string,
address: string,
//...要寫到什麼時候…
}

而我們改用映射類型寫:

使用映射
type TInfoMapped = { [K in TInfoKeys]: string };

就可以解決寫一大串的型別

另外,也可以用 Record:

使用 Record
type TInfoKeys = 'firstname' | 'lastname' | 'sex' | 'username' | 'zip' | 'address';
type TInfoValue = Record<TInfoKeys, string>;

基本語法

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

關鍵點:

  • keyof: 取得型別 T 的所有鍵組合(聯集)
  • in: 跑迴圈
  • T[K]: 取出 T 在鍵 K 上的屬性型別

常用內建映射類型

type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends PropertyKey, V> = { [P in K]: V };

修飾子加減號

// ?(可選)
type WithOptional<T> = { [K in keyof T]?: T[K] };

// 必填
type WithoutOptional<T> = { [K in keyof T]-?: T[K] };

// 加上 readonly
type WithReadonly<T> = { readonly [K in keyof T]: T[K] };

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

鍵重映射(Key Remapping)與樣板字面值

TS 4.1+ 支援用 as 重新命名鍵,並可搭配樣板字面值:

type PrefixKeys<T extends Record<string, any>> = {
[K in keyof T as `app_${Extract<K, string>}`]: T[K]
};

type Original = { id: number; name: string };
type Prefixed = PrefixKeys<Original>;
// => { app_id: number; app_name: string }
PrefixKeys<T extends Record<string, any>>
  • 這裡使用 extends 約束了泛型必須要為 string: any 的物件類型
app_${Extract<K, string>}
  • 這裡用了樣板字面值搭配 asextract 將 key 值重新命名
  • Extract:聯合類型選取指定內容,組成新的 type,這裡選取了泛型 K 並且指定為一定是 string 類型

來練習

練習一:把所有屬性改成可選/唯讀

// 實作 ToOptional 與 ToReadonly
type ToOptional<T> = unknown;
type ToReadonly<T> = unknown;

type User = {
id: string;
name: string;
active: boolean;
};

type U1 = ToOptional<User>;
// 期望:{ id?: string; name?: string; active?: boolean }

type U2 = ToReadonly<User>;
// 期望:{ readonly id: string; readonly name: string; readonly active: boolean }
點擊查看解答
type ToOptional<T> = { [K in keyof T]?: T[K] };
type ToReadonly<T> = { readonly [K in keyof T]: T[K] };

type User = {
id: string;
name: string;
active: boolean;
};

type U1 = ToOptional<User>;
type U2 = ToReadonly<User>;

練習二:移除修飾並統一必填

// 實作 MutableRequired:移除 readonly 與 ?,全部改成必填且可變
type MutableRequired<T> = unknown;

type Post = {
readonly id: string;
title?: string;
readonly published?: boolean;
};

type P1 = MutableRequired<Post>;
// 期望:{ id: string; title: string; published: boolean }
點擊查看解答
type MutableRequired<T> = { -readonly [K in keyof T]-?: T[K] };

type Post = {
readonly id: string;
title?: string;
readonly published?: boolean;
};

type P1 = MutableRequired<Post>;

練習三:鍵重映射 + 條件轉型

// 實作 ApiShape:
// - 把鍵加上前綴 'api_'
// - 若值型別可指派為 Date,改成 string,否則維持原型別

type ApiShape<T> = unknown;

type Entity = {
id: number;
name: string;
createdAt: Date;
};

type E1 = ApiShape<Entity>;
/* 期望:
{
api_id: number;
api_name: string;
api_createdAt: string;
}
*/
點擊查看解答
type ApiShape<T extends Record<string, any>> = { [K in keyof T as `api_${Extract<K, string>}`]: T[K] extends Data ? string : T[K] };

type Entity = {
id: number;
name: string;
createdAt: Date;
};

type E1 = ApiShape<Entity>;