cpcallen
cpcallen

Reputation: 2065

How to declare a contravariant function member in TypeScript?

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:

Upvotes: 3

Views: 421

Answers (1)

jcalz
jcalz

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.

Playground link to code

Upvotes: 1

Related Questions