Remirror
Remirror

Reputation: 754

Typing of overridden method, called by inherited method, in TypeScript (Bug?)

I stumbled accross the following scenario recently:

class A {
  public m1(x: string | string[]): string | string[] {
    return this.m2(x);
  }
  protected m2(x: string | string[]): string | string[] {
    return x;
  }
}
class B extends A {
  protected m2(x: string[]): string { // no compiler warning
    return x.join(',');
  }
}
const b = new B();
console.log(b.m1(['a', 'b', 'c'])); // ok
console.log(b.m1('d')); // runtime error

Is this a bug in the TypeScript typing system or intentional? If the latter, how can I change the typing so that the compiler will recognize the problem?

Upvotes: 3

Views: 231

Answers (2)

bela53
bela53

Reputation: 3485

In TypeScript, methods are bivariant and function properties contravariant with --strictFunctionTypes. Note their different syntax:

class MyClass {
  public fn(a: string): string { ... } // this is a method
  public fn = (a: string): string => { ... } // this is a function as property
}

To enable stricter typing for m2, you need to have --strictFunctionTypes enabled (default with strict) and use a function property:

protected m2 = (x: string | string[]): string | string[] => {
  return x;
}

Now, m2 will error properly and you will need to distinguish between string and string[]:

protected m2 = (x: string | string[]): string => { 
  return Array.isArray(x) ? x.join(',') : `${x},`;
}

Live code example on Playground

Related: What are covariance and contravariance?

Related: JS class fields

Upvotes: 1

potatoxchip
potatoxchip

Reputation: 565

This is expected an behaviour. In typescript, you override a method to funnel down the specificity of the method instead of widening it as in other languages.

As for the explanation...

class A {
  public m1(x: string | string[]): string | string[] {
    /**
     * Here variable 'x' is of type string | string[], which perfectly overlaps 
     * with the arguments for the method m2. So you won't get a compile-time error
     */
    return this.m2(x);
  }
  protected m2(x: string | string[]): string | string[] {
    return x;
  }
}
class B extends A {
  /**
   * Here method m2 is overridden by a method which accepts a string[], which
   * is covered by the parent's method, and hence no compile-time error. If
   * you made the argument type to 'string', it would still be accepted.
   */
  protected m2(x: string[]): string { // no compiler warning
    return x.join(',');
  }
}
const b = new B();
console.log(b.m1(['a', 'b', 'c'])); // ok
/**
 * Here you are passing the type you have specified method m1 will take. So there is no issue here as well
 */
console.log(b.m1('d')); // runtime error

Everything said, this is a logical-error, rather than a bug.

Upvotes: 0

Related Questions