Reputation: 2065
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 member
using method syntax (member(s: Super) {/* ... */}
) then it will be bivariant, but this does not help in situations where member
might be a collection of functions (e.g., in my actual code the type of member
is a dictionary of such functions: {[name: string]: (/*...*/, s: Super) => /*...*/}
).
I attempted to redeclare member
in Sub
with 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 in
and out
modifiers on templates to denote co/contravariance but I am not sure if they are applicable nor how to turn Super
into 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 .member
on 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.
Upvotes: 3
Views: 421
Reputation: 327634
It looks like you might want the polymorphic this
type, which acts like an implicit generic type parameter that is always constrained to the "current" class. So inside the Super
class body, the this
type refers to "some subtype of Super
", while inside the Sub
class body it refers to "some subtype of Sub
". For instances of Super
, the this
type will be instantiated with Super
, and for Sub
it will be instantiated with Sub
.
That is, inside the class body, this
acts 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:
class Super {
public member: (x: this) => void = function () { }
use() { const f = this.member; f(this) }
}
class Sub extends Super {
method() { }
}
const sub = new Sub();
sub.member = function (x: Sub) { x.method() }; // okay
Looks good.
Note that you could simulate this behavior by using generics explicitly (using a recursive, F-bounded constraint reminiscent of Java):
class Super<T extends Super<T>> {
public member: (x: T) => void = function () { }
use(this: T) { const f = this.member; f(this) }
}
class Sub extends Super<Sub> {
method() { }
}
const sub = new Sub();
sub.member = function (x: Sub) { x.method() };
which is less pretty but gives you some more flexibility, if this
types by themselves don't meet your needs.
Upvotes: 1