TypeScript泛型方法返回扩展类实际类型的推断问题
核心问题
需要让类的create方法返回函数实际执行结果的推断类型,但当前返回类型被识别为泛型约束对应的基接口类型,而非函数实际返回的扩展类实例类型,尝试多种泛型包装、解包方案均未生效。
问题现象
newDocumentSet的类型被推断为IDocumentSet<DocumentTypes, IContact, never>,而非create方法传入的extend回调函数返回的实际扩展类类型。
问题说明
期望create方法返回扩展后的实例类型,而非IDocumentSet基接口类型。目前泛型定义create<TExtension extends IDocumentSet<TDocumentType, TEntity, TExtraExclusions>>会导致类型被错误收窄为基接口,无法找到合适的写法:既约束返回类型必须继承IDocumentSet<TDocumentType, TEntity, TExtraExclusions>,又能让TypeScript正确推断返回实际的扩展类型而非基接口类型。
实现目标
让create方法返回扩展逻辑生成的扩展类实例的实际类型,而非基接口类型。
注:示例提取自大型代码库,为精简示例省略了
TExtraExclusions相关的业务逻辑,仅保留了泛型定义。
复现代码
export interface IDocumentSetBase<TDocumentType extends string> { get DocumentType(): TDocumentType; empty(): Promise<void>; } interface IDocumentSet<TDocumentType extends string, TEntity extends IDbRecord<TDocumentType>, TExtraExclusions extends (keyof TEntity) = never> extends IDocumentSetBase<TDocumentType> { info(): boolean } interface IDbRecord<TDocumentType> extends IDbAdditionRecord<TDocumentType> { readonly _id: string; readonly _rev: string; } interface IDbAdditionRecord<T> { readonly DocumentType: T; } export class DocumentSet<TDocumentType extends string, TEntity extends IDbRecord<TDocumentType>, TExtraExclusions extends (keyof TEntity) = never> implements IDocumentSet<TDocumentType, TEntity, TExtraExclusions> { private _documentType: TDocumentType; get DocumentType() { return this._documentType; } constructor(documentType: TDocumentType) { this._documentType = documentType; } info() { return true; } async empty() { } } class Builder<TDocumentType extends string, TEntity extends IDbRecord<TDocumentType>, TExtraExclusions extends (keyof TEntity) = never> { private _documentType: TDocumentType; constructor(documentType: TDocumentType) { this._documentType = documentType; } create<TExtension extends IDocumentSet<TDocumentType, TEntity, TExtraExclusions>>(extend: (Instance: typeof DocumentSet<TDocumentType, TEntity, TExtraExclusions>, documentType: TDocumentType) => TExtension = w => (w as any) as TExtension) { const result = extend(DocumentSet<TDocumentType, TEntity, TExtraExclusions>, this._documentType); return result; } } enum DocumentTypes { Contacts = "Contacts" } interface IContact extends IDbRecord<DocumentTypes> { firstName: string; lastName: string; address: string; phone: string; } const builder = new Builder<DocumentTypes, IContact>(DocumentTypes.Contacts); const newDocumentSet = builder.create((Instance, documentType) => { return new class extends Instance { constructor() { super(documentType) } test() { return "some value"; } } });
解决方案
问题根源是create方法默认参数中的as TExtension类型断言干扰了TypeScript的泛型推断逻辑,编译器会优先从默认参数的泛型默认值推断TExtension为基接口类型,而非从传入的回调返回值推断实际类型。
通过函数重载分离泛型类型签名和实现签名即可解决,修改后create方法写法如下:
// 泛型类型签名:用于传入自定义扩展回调时的类型推断 create<TExtension extends IDocumentSet<TDocumentType, TEntity, TExtraExclusions>>( extend: (Instance: typeof DocumentSet<TDocumentType, TEntity, TExtraExclusions>, documentType: TDocumentType) => TExtension ): TExtension; // 实现签名:处理默认参数逻辑,不参与泛型推断 create( extend: (Instance: typeof DocumentSet<TDocumentType, TEntity, TExtraExclusions>, documentType: TDocumentType) => IDocumentSet<TDocumentType, TEntity, TExtraExclusions> = w => new w(w.prototype.DocumentType) ) { const result = extend(DocumentSet<TDocumentType, TEntity, TExtraExclusions>, this._documentType); return result; }
调整后效果:
- 传入自定义扩展回调时,TypeScript会自动从回调返回的匿名类推断
TExtension的实际类型,可正常访问扩展的test()方法 - 不传回调使用默认实现时,返回类型为基接口
IDocumentSet,符合预期 - 保留
TExtension必须继承自IDocumentSet的类型约束,不符合规范的扩展实现会抛出类型错误
内容的提问来源于stack exchange,提问作者Agrejus

