Zak Henry
Zak Henry

Reputation: 2185

Typescript - how to correctly type a member of abstract generic class based on possible extra implementations

This is really hard to describe in words, but essentially I want to have a base abstract class that describes an abstract interface that needs to change depending on whether or not a derived class is implementing a certain interface or not.

Here's a TS playground showing the issue - everything is working fine, and the types are correct outside of the class, but internally typescript is reporting type errors.

playground

Upvotes: 2

Views: 245

Answers (1)

jcalz
jcalz

Reputation: 329553

Polymorphic this is not really helping you here; there's no way to mark a class as final in TypeScript the way you can in, say, Java... so it's always possible that polymorphic this has to refer to some narrowed type in some as-yet-unknown subclass. For example:

class Foo extends Derived {
  map(val: number) {
    return "oops";
  }
}
new Foo(7).getDefaultVal().toUpperCase(); // error at runtime

Here we have, with no compiler errors, extended Derived. But Foo.getDefaultVal() is just inheriting the implementation in Derived, whose return type is supposed to be DeriveMapVal<this, number>, where this is going to be Foo at the call site. And DeriveMapVal<Foo, number> is string. But Foo's implementation is just returning a number and not a string, which is why you're getting the error: the compiler simply cannot verify that number is assignable to DeriveMapVal<this, number>.


As for how to deal with it, I guess it depends on what you want to do. The easiest solution I can think of is to narrow the subclass implementation to refer to the actual self-class and not this:

class Derived extends Generic<number> {
  getDefaultVal(): DeriveMapVal<Derived, number> {
    return 0; // okay
  }
}

This should work, although it's a bit strange that you can still subclass Derived as before, and the compiler correctly sees that Foo is not assignable to Derived:

class Foo extends Derived {
  map(val: number) {
    return "oops";
  }
}

new Foo(7).getDefaultVal().toUpperCase(); // error at compile time too, now
const d: Derived = new Foo(10); // error
const g: Generic<number> = new Foo(20); // error

I mean, class Foo extends Derived apparently does not guarantee that Foo extends Derived is true. Weird stuff. As long as you can handle that wrinkle then this might be a way for you to proceed.

Okay, hope that helps; good luck!

Playground link to code

Upvotes: 1

Related Questions