抽象泛型函数的Typescript模式似乎被打破了

时间:2018-04-05 14:46:50

标签: typescript generics abstract

我有一个这样的模式,我在代码中使用A LOT,我在几个月(甚至几年)之前使用旧版本的typescript编译器编写,可能是1.8 - 2.2:

interface IBase { };

interface IExtends extends IBase {
    key2: string;
};

abstract class MyAbstractClass {
    protected abstract myFunc<T extends IBase>(arg: string): T;
};

class MyClass extends MyAbstractClass {
    protected myFunc(arg: string): IExtends {
        return { key2: arg };
    }
};

当我写这段代码时,Typescript根本就没有抱怨,并按照你的预期处理了这个问题。

但是现在,typescript(版本2.8.1)抱怨:

src/errorInvestigation.ts(12,15): error TS2416: Property 'myFunc' in type 'MyClass' is not assignable to the same property in base type 'MyAbstractClass'.
  Type '(arg: string) => IExtends' is not assignable to type '<T extends IBase>(arg: string) => T'.
    Type 'IExtends' is not assignable to type 'T'.

如果打字稿在将此标记为错误时是正确的,那么完成相同操作的正确方法是什么?

非常感谢。

2 个答案:

答案 0 :(得分:2)

我认为你可能并不认为myFunc是通用的。以下签名

abstract class MyAbstractClass {
    protected abstract myFunc<T extends IBase>(arg: string): T;
};

表示MyAbstractClassmyFunc()方法,该方法接受string输入并返回调用者指定的IBase的任何子类型。但是您希望它返回实现者指定的IBase子类型。如果myFunc不是protected,您可以快速看到问题

declare const myAbstractInstance: MyAbstractClass;
let iBase = myAbstractInstance.myFunc<IBase>('hey'); // IBase
let iExtends = myAbstractInstance.myFunc<IExtends>('hey'); // IExtends?!
let iOther = myAbstractInstance.myFunc<IBase & {foo: string}>('hey'); // What?!
iOther = iExtends; // error, not assignable

使用相同的参数调用相同的函数三次,并且在运行时它们将具有相同的输出,但显然TypeScript认为输出具有不同的类型。这是一种奇怪的模式,而不是你打算传达的东西。

而且我不确定它何时被强制执行,但你不应该被允许用非泛型函数覆盖泛型函数,除非非泛型函数确实是泛型函数的子类型一。但由于T可以是调用者指定的IBase的任何子类型,因此唯一可分配给每个可能的T的非泛型类型将是{ {1}}和never。我想你并不想归还那些。

听起来你只是希望any返回子类实现指定的myFunc或某个子类型。如果是这样,那么你可以完全没有泛型:

IBase

这对你有用。子类

abstract class MyAbstractClass {
  protected abstract myFunc(arg: string): IBase;
};

class MyClass extends MyAbstractClass { protected myFunc(arg: string): IExtends { return { key2: arg }; } }; 兼容,因为方法返回类型为covariant,这意味着如果MyAbstractClassY的子类型,那么X的方法是允许返回Y的类似方法的子类型。

如果您需要方法参数在子类中更窄,这将是不同的,因为方法参数是逆变的。如果XT的类型而不是返回类型,则上述不应该工作......但确实正好工作,因为方法参数仍被视为bivariant。但我认为这不是你的用例。 (更新:我看到你的用例。显然,方法参数因为这个原因是双变量的,所以试试吧。我个人不喜欢双重性,而且还有更多&#34;纠正&#34;解决方法,但它们可能比它们的价值更麻烦。)

希望有所帮助;祝你好运。

答案 1 :(得分:1)

非常感谢jcalz让我开始了研究之路,导致一个令人遗憾但可行的答案

我仍然感到困惑的是为什么打字稿会做出这种改变,因为我认为描述这种改变的原始方式没有问题。

答案是将泛型提升到类级别,而不是将它们留在方法上。这有利有弊,在我看来比专业人士更有利。优点是抽象类的主体可能更清晰,更容易阅读。缺点是如果你有许多不同类型需要在同一个类中处理,那么这个类本身需要在类声明中声明所有这些类型,并且每个派生类也需要这样做,这意味着这更难阅读我认为。但它会完成这项工作。

这是我看到的完整答案,我希望任何人(包括jcalz!)能够进一步思考/改进:

interface IArgBase {
    baseArgKey: string;
};

interface IReturnBase {
    baseReturnKey: string;
};

interface IPromiseBase {
    basePromiseKey: string;
};

interface IArgExtends extends IArgBase {
    extendedArgKey: string;
};

interface IReturnExtends extends IReturnBase {
    extendedReturnKey: string;
};

interface IPromiseExtends extends IPromiseBase {
    extendedPromiseKey: string;
};

abstract class MyAbstractClass<TArg extends IArgBase, TReturn extends IReturnBase, TPromise extends IPromiseBase>{
    protected abstract myFunc(arg: TArg): TReturn;
    protected abstract myAsyncFunc(arg: TArg): Promise<TPromise>;
};

class MyClass extends MyAbstractClass<IArgExtends, IReturnExtends, IPromiseExtends> {
    protected myFunc(arg: IArgExtends): IReturnExtends {
        return { baseReturnKey: arg.baseArgKey, extendedReturnKey: arg.extendedArgKey };
    };
    protected myAsyncFunc(arg: IArgExtends): Promise<IPromiseExtends> {
        return Promise.resolve({ basePromiseKey: arg.baseArgKey, extendedPromiseKey: arg.extendedArgKey });
    };
};