Search code examples
typescriptoverridingabstract

Why are the generic parameter types of this method override incompatible (TS2416)?


I am attempting to override a generic method, however Typescript complains that the types are incompatible. I feel like I must be missing something as I would have thought that the types would be the same.

I see the same error whether the method is originally declared in a base class or an interface.

Abstract class example

Typescript playground link

type OtherClass<T1, T2> = { t1?: T1, t2?: T2 }

abstract class ClassA<T1> {
  protected value?: T1;

  constructor () {
    this.value;
  }

  protected abstract MethodA<T2 extends OtherClass<T1, T3>, T3>(parameterA: T2, parameterB: T3): void;
}

abstract class ClassB<T> extends ClassA<T> {

  constructor () {
    super();
    this.value;
  }

  protected override MethodA<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3): void {
    parameterA;
    parameterB;
  }
}

The error

Property 'MethodA' in type 'ClassB<T>' is not assignable to the same property in base type 'ClassA<T>'.
  Type '<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3) => void' is not assignable to type '<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3) => void'. Two different types with this name exist, but they are unrelated.
    Types of parameters 'parameterA' and 'parameterA' are incompatible.
      Type 'T2' is not assignable to type 'OtherClass<T, T3>'.
        Type 'OtherClass<T, T3>' is not assignable to type 'OtherClass<T, T3>'. Two different types with this name exist, but they are unrelated.
          Type 'T3' is not assignable to type 'T3'. Two different types with this name exist, but they are unrelated.
            'T3' could be instantiated with an arbitrary type which could be unrelated to 'T3'.(2416)

Interface example

Typescript playground link

type OtherClass<T1, T2> = { t1?: T1, t2?: T2 }

interface InterfaceA<T1> {
  value?: T1;

  MethodA<T2 extends OtherClass<T1, T3>, T3>(parameterA: T2, parameterB: T3): void;
}

abstract class ClassB<T> implements InterfaceA<T> {

  public value?: T;

  protected override MethodA<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3): void {
    parameterA;
    parameterB;
  }
}

The error

Property 'MethodA' in type 'ClassB<T>' is not assignable to the same property in base type 'InterfaceA<T>'.
  Type '<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3) => void' is not assignable to type '<T2 extends OtherClass<T, T3>, T3>(parameterA: T2, parameterB: T3) => void'. Two different types with this name exist, but they are unrelated.
    Types of parameters 'parameterA' and 'parameterA' are incompatible.
      Type 'T2' is not assignable to type 'OtherClass<T, T3>'.
        Type 'OtherClass<T, T3>' is not assignable to type 'OtherClass<T, T3>'. Two different types with this name exist, but they are unrelated.
          Type 'T3' is not assignable to type 'T3'. Two different types with this name exist, but they are unrelated.
            'T3' could be instantiated with an arbitrary type which could be unrelated to 'T3'.(2416)

It seems to be saying that T3 for MethodA on ClassA could be different from the T3 for MethodA on ClassB, but I don't see how that's possible.

I can get rid of the error by updating the T3 generic parameter in ClassA to extends any or extends unknown, but I don't know if that's wise here as I don't know if I'd be missing a larger issue. Throwing in those extends feels like I may be sweeping a genuine problem under the rug.

I suspect I must be missing something fundamental here, but I don't know what it is.


Solution

  • This is a longstanding known bug in TypeScript, reported at microsoft/TypeScript#25373. Sometimes when generic class methods have constraints on type parameters that involve other type parameters, these methods cannot be overriden without a compiler error, even if the subclass method signature is identical to that of the superclass. It seems that TypeScript is unable to unify the call signatures. Unification is tricky in general, but one would hope that an identical call signature would be allowed.

    Anyway, this bug is on the Backlog, meaning that it is unlikely to be actively worked on by the TS team, but they will entertain pull requests by the community. So anyone who wants to see this bug fixed might consider doing it themselves and then submitting their fix.

    Until and unless it's fixed, you'll have to work around it. One possibility is to intersect the type of parameterA with OtherClass<T1, T3>. In some sense I'm just moving the constraint from T2 to typeof parameterA, and now the T2 extends otherClass<T1, T3> is redundant:

    protected abstract MethodA<T2 extends OtherClass<T1, T3>, T3>(
      parameterA: T2 & OtherClass<T1, T3>, parameterB: T3): void;
    

    Or one of a number of other reworkings of the call signature to avoid the problem, including loosening things with any or unknown, if all else fails.

    Playground link to code