I am facing a challenge with a class that contains a member which is a function taking an instance of the same class:
class Super {
public member: (x: Super) => void = function(){}
use() {const f = this.member; f(this)}
}
However, I need the member to be contravariant so that subclass instances can accept member
values that are functions taking that specific subclass, like this:
class Sub extends Super {
method() {}
}
const sub = new Sub();
sub.member = function(x: Sub) {x.method()};
Yet, TypeScript rightfully raises an error:
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'.
Is there a way to declare member
such that it allows a covariant parameter type in subclasses?
My attempts to solve this have included:
I know that using method syntax to declare
member
(member(s: Super) {/* ... */}
) would make it bivariant, but this doesn't work whenmember
may be a collection of functions (e.g., my actual code involves a dictionary of such functions:
).{[name: string]: (/*...*/, s: Super) => /*...*/}
I tried redeclaring
member
inSub
with a more restrictive signature:class Sub extends Super { public member: (x: Sub) => void = function(x){x.method()}; method() {} }
But TypeScript still rejects it:
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 am aware that TypeScript now supports the
in
andout
modifiers on templates for co/contravariance, but I am unsure if they apply here and how to modify the declaration ofSuper
accordingly.I prefer not to disable
strictFunctionTypes
as it is generally helpful and I don't want users of this library to adjust their settings just to assign to.member
on subclass instances.If all else fails, I can resort to casting the assigned values as
(x: Super) => void
, but this removes safeguards against incorrect assignments to different subclasses, resulting in runtime failures like this: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;
This code is accepted by TypeScript but fails at runtime.
Browsing through related questions, I came across a similar query involving interfaces rather than subclasses, but the answers provided are not formal yet. Furthermore, the linked snippets seem to rely on explicitly listing all subtypes, which is impractical for situations where there are numerous subclass variations.