Mapped Types
tags: TypeScript
參考資料: TypeScript Handbook、冴羽 TypeScript 系列
映射型別 Mapped Types
有的時候,一個型別要基於另外一個型別,但你又不想拷貝一份,這個時候可以考慮使用映射型別 (Mapped Type)。
Mapped Type 建立在 index signature 的語法上,用在宣告未提前宣告的屬性型別:
// 當你需要提前宣告屬性的型別時
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: 123,
};
而 Mapped Type,就是使用了 PropertyKeys
聯合型別的泛型,其中 PropertyKeys
多是通過 keyof
建立,然後循環遍歷 key 值建立一個型別:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
在這個例子裡,OptionsFlags
會遍歷 Type
所有的屬性,然後設為 boolean
型別。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<FeatureFlags>;
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
映射修飾符 Mapping Modifiers
在使用 Mapped Type 的時候有兩個額外的修飾符可以使用,一個是 readonly
,用於設置屬性唯讀,一個是 ?
,用於設置屬性可選。
你可以透過前綴 (prefix) -
或是 +
來刪除或是添加這些修飾符,如果你不寫前綴,則預設為 +
// 移除屬性中的 'readonly' 屬性
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
/*
type UnlockedAccount = {
id: string;
name: string;
}
*/
// 移除屬性中的可選屬性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
/*
type User = {
id: string;
name: string;
age: number;
}
*/
通過 as
實現 Key 的重新映射 Key Remapping via as
在 TypeScript 4.1 以後,你可以使用 as
語句重新映射型別中的 key:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
你可以利用樣板字面值型別 (template literal types),基於之前的屬性名建立一個新屬性名:
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
}
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
你也可以利用 conditional type 回傳一個 never
去過濾掉某些屬性:
// 移除 'kind' 屬性
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, 'kind'>]: Type[Property];
};
interface Circle {
kind: 'circle';
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
/*
type KindlessCircle = {
radius: number;
}
*/
你還可以去遍歷任何聯合,不僅僅是 string | number | symbol
這種聯合,可以是任何型別的聯合:
type EventConfig<Events extends { kind: string }> = {
[E in Events as E['kind']]: (event: E) => void;
};
type SquareEvent = { kind: 'square'; x: number; y: number };
type CircleEvent = { kind: 'circle'; radius: number };
type Config = EventConfig<SquareEvent | CircleEvent>;
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
深入探索 Further Exploration
Mapped Type 可以很好地跟本章節 (Type Manipulation) 的其他功能搭配使用,舉例來說,現在有一個使用 Conditional Type 的 Mapped Type,它會根據一個物件的屬性 pii 是否設置為 true 回傳 true false:
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: 'incrementing' };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>;
/*
type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
}
*/