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:
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.
But now, typescript (version 2.8.1) complains:
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. 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. But you want it to return a subtype of IBase
that the implementer specifies. If myFunc
weren't protected
you could see the problem quickly like
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. 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
. 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. 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
.
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. 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
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:
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 });
};
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With