合并记录的泛型类型收窄问题排查与修复求助
泛型Model扩展时getEmbed方法类型收窄失败问题
我在应用中用class包装API数据,需要支持泛型扩展嵌入属性。比如基础ApiModel派生出ProductModel,还要再扩展出带额外属性的B2BProductModel。
我通过泛型传递嵌入属性类型:
class Model<TEmbeds extends Record<string, any> = {}> {
并实现了getEmbed方法来访问这些属性:
getEmbed<K extends keyof TEmbeds>(key: K): TEmbeds[K]; getEmbed<K extends keyof TEmbeds, M extends ConstructorType<Model>>( key: K, model: M ): TEmbeds[K] extends any[] ? InstanceType<M>[] : TEmbeds[K] extends Record<any, InstanceType<M>> ? Record<keyof TEmbeds[K], InstanceType<M>> : InstanceType<M> | null getEmbed<K extends keyof TEmbeds, M extends ConstructorType<Model>>( key: K, model?: M ): TEmbeds[K] | InstanceType<M>[] | Record<keyof TEmbeds[K], InstanceType<M>> | InstanceType<M> | null { // 实现逻辑省略 }
但扩展TEmbeds类型时,getEmbed无法正确收窄类型,返回的是所有可能类型的联合类型。
示例代码:
interface Embeds { baz: SomeModel[] } type DisallowObjectProps<T extends Record<string, any>> = Partial<{ [key in keyof T]: never }> type MergeLeft<A extends Record<string, any>, B> = B extends Record<string, any> ? { [K in keyof A | keyof B]: K extends keyof A ? A[K] : K extends keyof B ? B[K] : never } : A class FooModel< TExtraEmbeds extends Record<string, any> & DisallowObjectProps<Embeds>, > extends Model< MergeLeft<Embeds, TExtraEmbeds> > { get baz() { // 问题:返回类型并非SomeModel[],而是:SomeModel | SomeModel[] | Record<keyof MergeLeft<Embeds, TExtraEmbeds>["baz"], SomeModel> | null return this.getEmbed('baz', SomeModel); } processBaz() { return this.baz.map((item) => item.doSomething()); // <---- 此处报错 } }
解决方案
问题核心是MergeLeft类型与未具体化的泛型参数组合后,TypeScript无法准确推断MergeLeft<Embeds, TExtraEmbeds>["baz"]的具体类型,导致getEmbed重载无法匹配正确分支。以下是几种修复方式:
1. 显式指定getEmbed泛型参数
调用getEmbed时明确传入K和M的类型,帮TypeScript锁定返回类型:
class FooModel< TExtraEmbeds extends Record<string, any> & DisallowObjectProps<Embeds>, > extends Model< MergeLeft<Embeds, TExtraEmbeds> > { get baz() { return this.getEmbed<'baz', typeof SomeModel>('baz', SomeModel); } processBaz() { return this.baz.map((item) => item.doSomething()); // 不再报错 } }
2. 优化类型合并与约束逻辑
修改MergeLeft为更严格的类型合并方式,同时显式声明baz的返回类型:
// 保留原有字段类型,仅合并新字段的MergeLeft type MergeLeft<A extends Record<string, any>, B extends Record<string, any>> = Omit<B, keyof A> & A; class FooModel< TExtraEmbeds extends Record<string, any> & DisallowObjectProps<Embeds>, > extends Model<MergeLeft<Embeds, TExtraEmbeds>> { get baz(): InstanceType<typeof SomeModel>[] { return this.getEmbed('baz', SomeModel) as InstanceType<typeof SomeModel>[]; } processBaz() { return this.baz.map((item) => item.doSomething()); } }
3. 改进getEmbed重载的类型推断
给getEmbed的重载添加更精确的条件判断,利用infer明确类型分支:
class Model<TEmbeds extends Record<string, any> = {}> { getEmbed<K extends keyof TEmbeds>(key: K): TEmbeds[K]; getEmbed<K extends keyof TEmbeds, M extends ConstructorType<Model>>( key: K, model: M ): TEmbeds[K] extends Array<infer _> ? InstanceType<M>[] : TEmbeds[K] extends Record<string, infer _> ? Record<keyof TEmbeds[K], InstanceType<M>> : InstanceType<M> | null; // 实现逻辑省略 }
这种写法能让TypeScript在识别到TEmbeds[K]是数组类型时,直接匹配数组分支,避免返回联合类型。
内容的提问来源于stack exchange,提问作者Matej
相关产品推荐
相关产品推荐

