Website-Suche

Kann ich in TypeScript eine klassische Funktion als Parameter erzwingen, anstelle einer Pfeilfunktion, wenn ich weiß, dass `this` dynamisch ist?


Einige APIs wollen Funktionen erhalten, die mit this interagieren müssen, daher kann dort keine Pfeilfunktion verwendet werden, z. B. für eine Knex-Subquery.

Ist es möglich, TypeScript darauf hinzuweisen, dass eine Funktion mit ungebundenem this erwartet wird?

Antwort #1

Kurz gesagt: Es gibt leider keine Möglichkeit, dies durchzusetzen, ohne dass viele zusätzliche manuelle Eingaben von Klassenmethoden erforderlich wären.

TypeScript hat this-Parameter, wobei die Aufrufsignatur einer Funktion einen initialen Funktionsparameter namens this hat, der eine Einschränkung für den Typ des this-Kontexts angibt, der erforderlich ist, wenn Sie die Funktion aufrufen. Der this-Parameter ist „falsch“, da Sie ihn beim Aufrufen der Funktion nicht tatsächlich als ersten Parameter übergeben. Zum Beispiel:

function sayMyName(this: { name: string }) {
  console.log("Hi, my name is " + this.name + ".");
}

Die Funktion sayMyName() nimmt keine Argumente entgegen, kann aber nur aufgerufen werden, wenn sie an ein Objekt des Typs {name: string} gebunden ist. Dies fängt den folgenden Fehler zur Compile-Zeit ab:

try {
  sayMyName(); // error, won't work unless bound
} catch (e) {
  console.log(e); // TypeError: this is undefined
}

Und erlaubt solche Dinge:

sayMyName.bind({ name: "Harry Potter" })(); // okay
// Hi, my name is Harry Potter.

const hermione = {
  name: "Hermione Granger",
  speak: sayMyName
}
hermione.speak(); // okay
// Hi, my name is Hermione Granger.

Die offensichtliche Vorgehensweise zur Lösung dieses Problems besteht also darin, dass die übergebenen Funktionen angeben, dass ihr this-Kontext void sein sollte, so wie hier:

function callbackCaller(cb: (this: void) => void): void {
  cb(); // okay
}

Und dies verhält sich genau so, wie Sie es mit Pfeil-/ungebundenen Funktionen und mit sayMyName wünschen:

const arrow = () => console.log("I am an arrow function");
callbackCaller(arrow); // okay
// I am an arrow function

const anonymous = function () { console.log("I am an anonymous function"); }
callbackCaller(anonymous); // okay
// I am an anonymous function

try {
  callbackCaller(sayMyName); // error, 'this' types are incompatible
} catch (e) {
  console.log(e); // TypeError: this is undefined
}

Aber dann stößt du auf ein Problem, wenn du es mit Klassenmethoden versuchst:

class Weasley {
  constructor(private givenName: string) { }
  greet() {
    console.log("Hi, my name is " + this.givenName + " Weasley.");
  }
}

const ron = new Weasley("Ron");
ron.greet(); // Hi, my name is Ron Weasley.

try {
  callbackCaller(ron.greet); // oops, no compile error!!!
} catch (e) {
  console.log(e); // TypeError: this is undefined
}

TypeScript hat völlig versagt, zu erkennen, dass ron.greet einen this-Kontext vom Typ Weasley hat, und behandelt ihn stattdessen, als ob der this-Kontext vom Typ any wäre. Ursprünglich, als this-Parameter implementiert wurden, sollte es ein --strictThis-Compiler-Flag geben, um damit umzugehen, aber es wurde fallen gelassen. Es gibt ein offenes Issue auf Github, das vorschlägt, es hinzuzufügen, aber derzeit ist diese Funktion kein Teil der Sprache. (Wenn Sie möchten, dass es hinzugefügt wird, sollten Sie vielleicht zu diesem Issue gehen und ein 👍 geben oder Ihren Anwendungsfall beschreiben, wenn er überzeugender ist als das, was dort bereits steht.)

Die Umgehung besteht darin, alle Klassenmethoden, die Ihnen wichtig sind, zu nehmen und explizit this-Parameter für sie anzugeben. Ein vernünftiger Typ, den man verwenden kann, ist der polymorphe this-Typ, der schließlich so aussieht:

class Malfoy {
  constructor(private givenName: string) { }
  greet(this: this) { // <-- this parameter of type this
    console.log("It is I, " + this.givenName + " Malfoy.");
  }
}

Das behebt das Problem:

const draco = new Malfoy("Draco");
draco.greet(); // It is I, Draco Malfoy.

try {
  callbackCaller(draco.greet); // error, won't work unless bound
} catch (e) {
  console.log(e); // TypeError: this is undefined
}

auf Kosten davon, die Leute zu bitten, ihre Klassenmethoden mit this: this zu würzen. Das könnte ein entscheidender Punkt für Sie sein, da Sie andere nicht unbedingt bitten können, dies zu tun. Ich weiß es nicht.

Du könntest eventuell einige ausgefeilte Typ-Manipulationen verwenden, um TypeScript dazu zu bringen, this-Parameter zu allen Methoden einer Klasse hinzuzufügen, so wie hier:

type Thisify<T> = {
  [K in keyof T]: T[K] extends (...args: infer A) => infer R
  ? (this: Thisify<T>, ...args: A) => R : T[K]
}
const thisify = <T>(instance: T) => instance as Thisify<T>;

const reformedRon = thisify(ron);
reformedRon.greet(); // Hi, my name is Ron Weasley.
try {
  callbackCaller(reformedRon.greet); // error, won't work unless bound
} catch (e) {
  console.log(e); // TypeError: this is undefined
}

aber erneut, du könntest nicht die Gelegenheit haben, die Instanzen einer anderen Klasse mit thisify() zu versehen, bevor ihre Methoden an deine callbackCaller()-Funktion übergeben werden. Ich denke, ohne --strictThis sind wir irgendwie festgefahren.

Okay, ich hoffe, das hilft trotzdem. Viel Glück!

Link zum Code