Unterklasse - Wie deklariert man ein kontravariantes Funktionsmitglied in TypeScript?
Ich habe eine Klasse, die ein Mitglied enthält, das eine Funktion ist, die eine Instanz der Klasse übernimmt:
class Super {
public member: (x: Super) => void = function(){}
use() {const f = this.member; f(this)}
}
aber ich möchte, dass member kontravariant ist; speziell möchte ich Unterklassen-Instanzen erlauben, member-Werte zu akzeptieren, die Funktionen sind, die genau diese Unterklasse als Parameter nehmen, d.h.:
class Sub extends Super {
method() {}
}
const sub = new Sub();
sub.member = function(x: Sub) {x.method()};
aber tsc beschwert sich völlig zu Recht:
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'.
Wie erkläre ich member, sodass es in Unterklassen eine Funktion sein kann, die einen kovarianten (statt kontravarianten) Parametertyp annimmt?
Was ich versucht habe:
Ich weiß, dass wenn ich
membermit Methodensyntax erkläre (member(s: Super) {/* ... */}), es bivariant sein wird, aber dies hilft nicht in Situationen, in denenmembereine Sammlung von Funktionen sein könnte (z. B. ist in meinem eigentlichen Code der Typ vonmemberein Wörterbuch solcher Funktionen:{[name: string]: (/*...*/, s: Super) => /*...*/}).Ich habe versucht,
memberinSubmit einer restriktiveren Signatur neu zu deklarieren:class Sub extends Super { public member: (x: Sub) => void = function(x){x.method()}; method() {} }aber tsc weigert sich beharrlich, mir zu erlauben, die Waffe auf meinen Fuß zu richten:
Property 'member' in type 'Sub' is not assignable to the same property in base type 'Super'. 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'.Ich verstehe, dass TypeScript jetzt
in- undout-Modifikatoren bei Templates unterstützt, um Ko-/Kontravarianz anzuzeigen, aber ich bin mir nicht sicher, ob sie anwendbar sind oder wie manSuperin eine entsprechend templateisierte Deklaration umwandelt.Ich möchte
strictfunctionTypeslieber nicht deaktivieren, da es im Allgemeinen nützlich ist und ich die Benutzer dieser Bibliothek nicht dazu zwingen möchte, es zu deaktivieren, um eine Zuweisung zu.memberauf Unterklasseninstanzen vorzunehmen.Als letztes Mittel kann ich die Werte, die
as (x: Super) => voidzugewiesen werden, einfach casten, aber das entfernt den Schutz gegen die Zuweisung zur falschen Unterklasse, z. B.:class Sub1 extends Super { method1() {} } class Sub2 extends Super { method2() {} } const sub1 = new Sub1(); sub1.member = function(x: Sub2) {x.method2()} as (x: Super) => void;wird von tsc akzeptiert, schlägt aber zur Laufzeit fehl.
Wenn ich die verwandten Fragen überprüfe, sehe ich eine ähnliche Frage, die Schnittstellen anstelle von Unterklassen betrifft, aber sie hat noch keine formalen Antworten und ich verstehe die in den Kommentaren verlinkten Codeausschnitte nicht vollständig; sie scheinen darauf angewiesen zu sein, alle Untertypen vollständig auflisten zu können, was für meine Situation, in der eine beliebige Anzahl von (Unter)*Unterklassen existieren kann, nicht geeignet ist.
Antwort #1
Es sieht so aus, als ob Sie den polymorphen this-Typ verwenden möchten, der wie ein impliziter generischer Typ-Parameter wirkt, der immer auf die „aktuelle“ Klasse beschränkt ist. Innerhalb des Super-Klassenkörpers bezieht sich der this-Typ also auf „eine Unterklasse von Super“, während er innerhalb des Sub-Klassenkörpers auf „eine Unterklasse von Sub“ verweist. Für Instanzen von Super wird der this-Typ mit Super instanziiert, und für Sub wird er mit Sub instanziiert.
Das heißt, innerhalb des Klassenkörpers verhält sich this wie ein generischer Parameter, und außerhalb des Klassenkörpers verhält es sich so, als ob dieser Parameter mit einem Typargument angegeben wurde, das dem aktuellen Objekttyp entspricht.
Das gibt Ihnen das gewünschte Verhalten mit Ihrem Beispielcode:
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
Sieht gut aus.
Beachten Sie, dass Sie dieses Verhalten simulieren könnten, indem Sie Generika explizit verwenden (mit einer rekursiven, F-gebundenen Einschränkung, die an Java erinnert):
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() };
was weniger hübsch ist, Ihnen aber etwas mehr Flexibilität bietet, falls dies Typen für sich allein Ihre Anforderungen nicht erfüllen.
Playground-Link zum Code
