TypeScript如何定义键为联合类型、值类型各异的对象类型
问题描述
需要创建一个对象类型,满足两个约束:
- 所有键必须属于给定的字符串字面量联合类型,由编译器强制校验,不能多也不能少
- 每个键对应的值类型独立,和键本身绑定
具体场景:有一组PluginId字符串字面量联合类型代表不同插件,需要定义插件配置对象类型,键必须是PluginId的成员,每个键对应的值是该插件专属的配置类型。
初始实现代码:
// 插件ID字面量联合类型 export type PluginId = 'cat' | 'dog' | 'racoon' /** * 配置值规则: * - false 表示插件关闭 * - true 表示插件启用,使用默认配置 * - 传入自定义配置对象表示插件启用,使用自定义配置 */ export type Config<T = {}> = boolean | Partial<T> // 各插件专属配置类型,无专属配置的插件不需要定义 export type DogPluginConfig = { woof: boolean } export type CatPluginConfig = { meow: boolean } // 目标配置类型 export type Configs = { cat?: Config<CatPluginConfig> dog?: Config<DogPluginConfig> racoon?: Config } export function instantiatePlugins(configs: Configs): Plugin[] { // 业务逻辑省略 }
上述实现的缺陷是Configs和PluginId之间没有强制绑定:修改PluginId的成员时,编译器不会提示Configs是否漏写/多写了键,也无法阻止给Configs对象添加不属于PluginId的非法键。
尝试过的Record交叉类型方案:
export type Configs = Partial<Record<PluginId, Config>> & { cat?: Config<CatPluginConfig> dog?: Config<DogPluginConfig> }
该方案依然存在缺陷:交叉类型不会校验后半段对象的键范围,既允许写入不在PluginId中的多余键,也不会在PluginId新增成员时提示缺失对应配置项。
解决方案
使用TS的映射类型即可完美满足需求,实现逻辑如下:
- 首先定义插件ID到专属配置类型的映射表,覆盖所有
PluginId成员 - 通过映射类型遍历
PluginId联合类型,自动生成每个键对应的配置类型,从根源上保证键的合法性
基础实现
// 插件ID与专属配置的映射表,无专属配置的插件对应默认空对象类型 type PluginConfigMap = { cat: CatPluginConfig dog: DogPluginConfig racoon: {} } // 生成最终配置类型:键全部来自PluginId,每个键匹配对应专属Config类型,所有键可选 export type Configs = { [K in PluginId]?: Config<PluginConfigMap[K]> }
该实现自动满足所有约束:
- 无法添加
PluginId以外的键,编译器会直接报错 - 每个键的值类型自动匹配对应插件的配置类型,编写自定义配置时会获得正确的类型提示
- 修改
PluginId成员时,编译器会提示PluginConfigMap是否存在漏写的键
强化校验(可选)
如果需要强制PluginConfigMap和PluginId完全同步(既不能漏写键,也不能多写键),可以加一层泛型校验工具:
// 校验传入的类型必须包含所有PluginId的键,且不能有多余键 type ExactConfigMap<T extends Record<PluginId, unknown>> = { [K in keyof T]: K extends PluginId ? T[K] : never } // 用校验工具包裹映射表,一旦键和PluginId不匹配会直接抛出类型错误 type PluginConfigMap = ExactConfigMap<{ cat: CatPluginConfig dog: DogPluginConfig racoon: {} // 多写bird: {} 或者漏写racoon都会直接报类型错误 }>
内容的提问来源于stack exchange,提问作者Balázs Édes
相关产品推荐
相关产品推荐

