TypeScript同键异值对象的泛型消费类类型适配问题
问题背景
我有一组存储源数据的JSON文件,每种语言对应一个文件,所有文件键名相同但值类型不同:
// source-en.json { "action": ["change", "check"], "device": ["coil", "relay"] }
// source-pl.json { "action": ["wymienić", "sprawdzić"], "device": [ { "gender": "f", "singular": "cewkę", "plural": "cewki" }, { "gender": "m", "singular": "przekaźnik", "plural": "przekaźniki" } ] }
我用以下TypeScript接口描述这些数据:
interface SourceEn { action: string[], device: string[], } interface SourcePl { action: string[], device: NounForms[], } interface NounForms { gender: 'm' | 'f' | 'n' | 'pl'; singular: string; plural: string; }
目前我实现了一组继承自BaseConsumer的消费类(如EnConsumer、PlConsumer),BaseConsumer负责获取源文件并转换JSON数据,具体子类处理语言差异。当前BaseConsumer的实现如下:
type Source = SourceEn | SourcePl; class BaseConsumer<T extends Source> { public readonly action: Collection<ArrayItem<T['action']>>; public readonly device: Collection<ArrayItem<T['device']>>; public constructor(language: Language) { const data = this.getData(language); this.action = new Collection(data.action); this.device = new Collection(data.device); // 此处报错 } protected getData(language: Language): T {...} } class Collection<T> { public constructor(private store: T[]) {} } type ArrayItem<A extends unknown[]> = A extends (infer E)[] ? E : never; enum Language { EN = 'en', PL = 'pl', }
错误信息
在this.device = new Collection(data.device);行出现两个错误:
- 参数类型不匹配:
参数类型
string[] | NounForms[]无法赋值给类型string[]
类型NounForms[]无法赋值给类型string[]
类型NounForms无法赋值给类型string
- 属性类型不匹配:
类型
Collection<string>无法赋值给类型Collection<Source['device']>
类型string无法赋值给类型Source['device']
问题根源在于T extends Source的约束本质是联合类型,即使子类指定了具体的SourcePl或SourceEn,TypeScript在处理BaseConsumer泛型时仍会保留联合类型的歧义,导致类型推断失败。我需要一个无需冗余条件类型、可扩展的泛型实现方案,以适配未来新增语言的需求。
解决方案
通过语言-数据源类型映射的方式,将具体语言与对应的数据源类型强绑定,消除联合类型带来的歧义,同时保证扩展性:
1. 定义语言-数据源映射类型
新增一个映射类型,明确每种语言对应的数据源接口,后续新增语言只需扩展此映射:
// 语言与数据源类型的绑定映射 type LanguageSourceMap = { [Language.EN]: SourceEn; [Language.PL]: SourcePl; };
2. 重构BaseConsumer泛型逻辑
将泛型参数从T extends Source改为具体的语言枚举值,通过映射类型获取对应的数据源类型:
class BaseConsumer<L extends Language> { // 通过映射类型精准推断当前语言对应的字段类型 public readonly action: Collection<ArrayItem<LanguageSourceMap[L]['action']>>; public readonly device: Collection<ArrayItem<LanguageSourceMap[L]['device']>>; public constructor(language: L) { const data = this.getData(language); // 此时data的类型是明确的LanguageSourceMap[L],无联合类型歧义 this.action = new Collection(data.action); this.device = new Collection(data.device); } // getData返回的类型与当前语言严格绑定 protected getData(language: L): LanguageSourceMap[L] { // 示例实现:根据语言加载对应JSON文件 switch (language) { case Language.EN: return require('./source-en.json') as LanguageSourceMap[L]; case Language.PL: return require('./source-pl.json') as LanguageSourceMap[L]; default: throw new Error(`不支持的语言:${language}`); } } }
3. 简化子类实现
子类只需指定对应的语言枚举值,无需重复定义类型:
class EnConsumer extends BaseConsumer<Language.EN> {} class PlConsumer extends BaseConsumer<Language.PL> {}
扩展性说明
新增语言时只需完成三步:
- 新增对应语言的JSON数据源文件
- 新增对应的数据源接口(如
SourceEs) - 在
LanguageSourceMap中添加新的键值对,并扩展Language枚举
无需修改BaseConsumer的核心逻辑,完全符合开闭原则。
内容的提问来源于stack exchange,提问作者Forseti

