I have a class that contains a member that is a function that takes an instance of the class:
class Super {
public member: (x: Super) => void = function(){}
use() {const f = this.member; f(this)}
}
but I want member to be contravariant; specifically, I want to allow subclass instances to accept member values that are functions that take that specific subclass, i.e.:
class Sub extends Super {
method() {}
}
const sub = new Sub();
sub.member = function(x: Sub) {x.method()};
but tsc quite correctly complains:
Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'.
Types of parameters 'x' and 'x' are incompatible.
Property 'method' is missing in type 'Super' but required in type 'Sub'.
How do I declare member such that it can in subclasses be a function that takes a covariant (rather than contravariant) parameter type?
What I've tried:
I know that if I declare
memberusing method syntax (member(s: Super) {/* ... */}) then it will be bivariant, but this does not help in situations wheremembermight be a collection of functions (e.g., in my actual code the type ofmemberis a dictionary of such functions:{[name: string]: (/*...*/, s: Super) => /*...*/}).I attempted to redeclare
memberinSubwith a more restrictive signature:class Sub extends Super { public member: (x: Sub) => void = function(x){x.method()}; method() {} }but tsc steadfastly refuses to let me aim the gun at my foot:
Property 'member' in type 'Sub' is not assignable to the same property in base type 'Super'. Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'. Types of parameters 'x' and 'x' are incompatible. Property 'method' is missing in type 'Super' but required in type 'Sub'.I understand that typescript now supports
inandoutmodifiers on templates to denote co/contravariance but I am not sure if they are applicable nor how to turnSuperinto a suitably-templated declaration.I'd rather not turn off
strictfunctionTypes, as it is generally useful and I don't want to force users of this library to turn it off in order to assign to.memberon subclass instances.As a last resort I can just cast the values being assigned to
as (x: Super) => void, but this removes protection against assigning to the wrong subclass, e.g.:class Sub1 extends Super { method1() {} } class Sub2 extends Super { method2() {} } const sub1 = new Sub1(); sub1.member = function(x: Sub2) {x.method2()} as (x: Super) => void;is accepted by tsc but fails at runtime.
Checking the related questions, I see a similar question involving interfaces rather than subclasses, but it has no formal answers yet and I do not fully understand the snippets linked in the comments; they appear to depend on being able to fully enumerate all of the subtypes, which is not suitable for my situation where there may be an arbitrary number of (sub)*subclasses.
It looks like you might want the polymorphic
thistype, which acts like an implicit generic type parameter that is always constrained to the "current" class. So inside theSuperclass body, thethistype refers to "some subtype ofSuper", while inside theSubclass body it refers to "some subtype ofSub". For instances ofSuper, thethistype will be instantiated withSuper, and forSubit will be instantiated withSub.That is, inside the class body,
thisacts like a generic parameter, and outside the class body it behaves like that parameter has been specified with a type argument corresponding to the current object type.That gives you the desired behavior with your example code:
Looks good.
Note that you could simulate this behavior by using generics explicitly (using a recursive, F-bounded constraint reminiscent of Java):
which is less pretty but gives you some more flexibility, if
thistypes by themselves don't meet your needs.Playground link to code