简体   繁体   English

抽象泛型函数的打字稿模式似乎已损坏

[英]Typescript pattern for abstract generic functions seems to be broken

I have a pattern like this that I used A LOT in code I wrote months (even years) ago using older versions of the typescript compiler, maybe 1.8 - 2.2: 我有一个这样的模式,我在几个月(甚至几年前)使用较旧版本的打字稿编译器(也许是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 };
    }
};

Back when I wrote this code, Typescript did not complain at all, and handled this as you would expect. 回到我编写此代码时,Typescript根本没有抱怨,并按您期望的进行了处理。

But now, typescript (version 2.8.1) complains: 但是现在,打字稿(版本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'.

If typescript is correct in marking this as an error, then what would be the correct way to accomplish the same thing? 如果打字稿将其标记为错误是正确的,那么完成同一件事的正确方法是什么?

Many thanks. 非常感谢。

I think you probably don't really mean for myFunc to be generic. 我认为您可能并不真的意味着myFunc是通用的。 The following signature 以下签名

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

means that a MyAbstractClass has a myFunc() method which takes a string input and returns any subtype of IBase that the caller specifies. 表示MyAbstractClass具有myFunc()方法,该方法接受string输入并返回调用方指定的IBase任何子类型。 But you want it to return a subtype of IBase that the implementer specifies. 但是您希望它返回实现者指定的IBase的子类型。 If myFunc weren't protected you could see the problem quickly like 如果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

Where you call the same function with the same argument three times, and at runtime they would have the same output, but apparently TypeScript thinks the outputs are of different types. 如果您使用相同的参数调用三次相同的函数,并且在运行时它们将具有相同的输出,但是显然TypeScript认为输出是不同类型的。 This is a strange pattern and not what you are intending to convey anyway. 这是一个奇怪的模式,而不是您打算传达的内容。

And I'm not sure exactly when it got enforced, but you shouldn't be allowed to override a generic function with a non-generic one unless the non-generic one really is a subtype of the generic one. 而且我不确定确切的执行时间,但是除非允许非泛型函数确实是泛型函数的子类型,否则不应允许您使用非泛型函数覆盖泛型函数。 But since T can be any subtype of IBase specified by the caller, the only non-generic types that are definitely assignable to every possible T would be never and any . 但是,由于T可以是调用方指定的IBase 任何子类型,因此绝对可以分配给每个可能T的唯一非泛型类型将never any And you don't want to return those, I think. 我想,您也不想退还这些。


It sounds like you just want myFunc to return an IBase or some subtype of it specified by the subclass implementation. 听起来您只希望myFunc返回一个IBase或由子类实现指定的某些子类型。 If so, then you can do this without generics at all: 如果是这样,那么您完全不需要泛型即可执行此操作:

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

This will work for you. 这将为您工作。 The subclass 子类

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

is compatible with MyAbstractClass because method return types are covariant , meaning that if Y is a subtype of X , then a method of Y is allowed to return a subtype of the analogous method of X . MyAbstractClass兼容,因为方法返回类型是协变的 ,这意味着如果YX的子类型,则允许Y的方法返回X的相似方法的子类型。

This would be different if you needed the method parameter to be narrower in the subclass, because method parameters are contravariant. 如果您需要在子类中将方法参数缩小,则方法将有所不同,因为方法参数是互变的。 In the case that T was the type of arg and not the return type, the above shouldn't work... but it does happen to work because method parameters are still considered bivariant . Targ类型而不是返回类型的情况下,上面的方法不起作用...但是它确实起作用了,因为方法参数仍然被认为是bivariant But that is not your use case, I think. 我认为,但这不是您的用例。 (Update: I see that it is your use case. Method parameters are bivariant for this reason apparently, so try it out. Personally I don't like bivariance and there are more "correct" workarounds but they are probably more trouble than they are worth.) (更新:我看到这您的用例。显然,由于这个原因,方法参数是双变量的,请尝试一下。我个人不喜欢双变量,并且有更多“正确”的解决方法,但它们可能比实际存在的麻烦更大。价值。)

Hope that helps; 希望能有所帮助; good luck. 祝好运。

Many thanks to jcalz for starting me down a research path, leading to a regrettable but workable answer 非常感谢jcalz为我开辟了研究道路,并得出了令人遗憾但可行的答案

I am still baffled as to why typescript made this change, because I see no problem with the original way of describing this. 我仍然对打字稿为什么要进行此更改感到困惑,因为我认为描述它的原始方式没有问题。

The answer is to promote the generics to the class level rather than leaving them on the methods. 答案是将泛型提升到类级别,而不是将它们留在方法上。 This has pros and cons, in my opinion more cons than pros. 在我看来,这有优点也有缺点,而不是优点。 The pros are that the body of the abstract class is perhaps cleaner and easier to read. 优点是抽象类的主体可能更简洁,更易于阅读。 The cons are that if you have many different types that need to be handled within the same class, that class itself needs to declare all those types on the class declaration and every derived class needs to do that too, which means this is harder to read I think. 缺点是,如果在同一个类中需要处理许多不同的类型,则该类本身需要在类声明中声明所有这些类型,而每个派生类也需要这样做,这意味着更难阅读我认为。 But it will do the job. 但这会做的。

Here is the complete answer as I see it, and I would love anyone (including jcalz!) to chime in with further thoughts/refinements: 这是我所看到的完整答案,我希望任何人(包括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 });
    };
};

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM