Website-Suche

Vererbung - Typisierung einer überschriebenen Methode, die von einer geerbten Methode in TypeScript aufgerufen wird (Bug?)


Ich bin kürzlich über folgendes Szenario gestolpert:

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

Ist dies ein Fehler im TypeScript-Typisierungssystem oder beabsichtigt? Wenn Letzteres, wie kann ich die Typisierung ändern, sodass der Compiler das Problem erkennt?

Antwort #1

In TypeScript sind Methoden bivariant und Funktionsattribute kontravariant mit --strictFunctionTypes. Beachten Sie ihre unterschiedliche Syntax:

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

Um strengere Typisierung für m2 zu ermöglichen, müssen Sie --strictFunctionTypes aktiviert haben (Standard bei strict) und eine Funktions-Eigenschaft verwenden:

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

Jetzt wird m2 korrekt einen Fehler ausgeben und Sie müssen zwischen string und string[] unterscheiden:

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

Live-Code-Beispiel auf Playground

Verwandt: Was sind Kovarianz und Kontravarianz?

Verwandt: JS-Klassenfelder

Antwort #2

Dies ist ein erwartetes Verhalten. In TypeScript überschreibt man eine Methode, um die Spezifität der Methode zu verfeinern, statt sie wie in anderen Sprachen zu erweitern.

Was die Erklärung betrifft...

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

Alles gesagt, dies ist ein logischer Fehler, eher als ein Bug.