TypeScript Substitutability

Hendoul

Commander
Registriert
Apr. 2008
Beiträge
2.162
Hi :)

Ich habe diesen Artikel gelesen:
https://fettblog.eu/typescript-substitutability/

und frage mich, warum das hier einen compile error geben sollte:

Javascript:
function fetchResults(callback: (statusCode: number, results: number[]) => void) {
  const didItWork = callback(200, [1,2,3]); // ⚡️ compile error!
}
function handler(statusCode: number, results: number[]): boolean {
  return true;
}
fetchResults(handler); // compiles, no problem!

Wenn ich das Beispiel im TypeScript Playground eingebe funktioniert das so.
Und ich verstehe den Sinn nicht hinter => void , wenn man dann doch alles zurückgeben darf?
 
Zuletzt bearbeitet:
Das hängt von deinen TS Compiler Settings ab. TS ist ein Superset von JavaScript, bei dem der Compiler so lasch eingestellt werden kann, dass er jeden Blödsinn erlaubt, den man auch in JavaScript machen kann.

Klick im TS Playground mal oben links auf "Config". Da kannst du zumindest schon mal ein paar Restriktionen einschalten (beginnen meistens mit "no" oder "strict"). Aber auch im TS Playground werden nicht alle Optionen angeboten.

Den besagten Compiler Error kannst du provozieren indem du "noUnusedLocals" aktivierst.

Ist aber insgesamt vollkommen egal, weil - auch wenn der Compiler es zulässt - hat die Variable danach den Typ "void" mit dem man nicht mehr viel anfangen kann. Und spätestens Terser wird dann die Variable aus deinem Bundle entfernen, weil sie unnötig ist.
 
Zuletzt bearbeitet:
Das ist es ja eben, die Variable hat nicht den Wert undefined, was ich auch erwartet hätte nach all den Erläuterungen. Nein die Variable hat den Wert true, und das verwirrt mich.

Und eben, wozu überhaupt den Rückgabewert void definieren wenn sowieso alles zurückgegeben werden darf?
 
Ah! Jetzt versteh ich dein Problem, das eigentlich gar keins ist - solang du nicht irgendwo selbst auf die Idee kommst, die Typen zu verändern.

Betrachte es mal von dieser Seite: Dein Code oben führt zwar dazu, dass "didItWork" auf "true" gesetzt wird, aber(!) du kannst die Variable nicht weiter verwenden, weil sie innerhalb von "fetchResults" als "void" deklariert ist. Es ist also vollkommen egal, was diese Variable enthält.
Versuch mal danach ein "if(didItWork) { ... }". Da kriegst du dann einen Compile Error, weil "An expression of type 'void' cannot be tested for truthiness".

Es ist ja auch sinnvoll, dass man dein Beispiel genau so programmieren kann. Du sagst mit den Typen ja nur, dass du innerhalb der Funktion "fetchResults" nicht am Return Wert von "callback" interessiert bist. Also kann die übergebene "callback" Funktion returnen was sie will. Es hat keinen Einfluss auf das Verhalten von "fetchResults", weil der Return Wert nicht benutzt wird - sonst hätte man ihn ja nicht als "void" angegeben.

Oder anders gesagt: Solang du deine Typen angibst, wird schon dafür gesorgt, dass du keinen Unfug mit den Variablen anstellen kannst.

Wenn du also z.B. das hier schreibst, kriegst du 'nen deutlichen Typ Fehler angezeigt:
Code:
function fetchResults(callback: (statusCode: number, results: number[]) => string) {
  const didItWork = callback(200, [1,2,3]); // ⚡ compile error!
}
function handler(statusCode: number, results: number[]): boolean {
  return true;
}
fetchResults(handler); // ERROR!
(ich habe nur den Return Typ von "callback" zu "string" geändert)
 
Zuletzt bearbeitet:
Ich verstehs einfach nicht. Das hier gibt auch true aus:

Code:
function fetchResults(callback: (statusCode: number, results: number[]) => void) {
  const didItWork = callback(200, [1, 2, 3]);
  return didItWork;
}

function handler(statusCode: any): boolean {
  return true;
}

const isThisVoid = fetchResults(handler);
console.log(isThisVoid);

Warum gibt es mir dann true aus und nicht void oder undefined. Total verwirrend finde ich.
 
Der Grund ist ganz simpel: TypeScript ändert NICHTS(!!) an deinem Code.
Ob du die Typen hinschreibst, oder weglässt, beeinflusst nur die Error Messages vom Compiler, aber dein Code macht nach wie vor exakt dasselbe.
Wenn du TypeScript zu JavaScript kompilierst, passiert vereinfacht gesagt folgendes: Es werden ganz stupide sämtliche Typ Informationen aus deinem Code entfernt. Und das war's. Keine Magie. Nix. Der Code bleibt 1 zu 1 identisch.
Das siehst du ja auch sofort, wenn du deinen Code im TypeScript Playground eingibst: http://www.typescriptlang.org/play/
Auf der rechten Seite steht das kompilierte Ergebnis in JavaScript. Das sieht 1 zu 1 so aus wie dein TypeScript Code - nur halt ohne Typ Informationen.

Das Problem in diesem Fall ist "console.log". Das akzeptiert Werte vom Typ "any" - also alles und jeden. Aber genau dafür ist "console.log" ja auch gedacht: Es sollte schlicht und ergreifend alles auf der Konsole ausgeben können. Und das ist eine von ganz ganz wenigen Funktionen, wo "any" verwendet wird - und es auch sinnvoll ist.

Wenn man seinen Code vollständig in TypeScript schreibt, gibt's (fast) nirgends mehr "any" und schon taucht das von dir benannte "Problem" auch gar nicht mehr auf.

Man kann in TypeScript - dank "any" - auch folgenden Unsinn schreiben:
Code:
const b = true;
(b as any) = "foo";
console.log(b);
"b" ist nach wie vor vom Typ "true", aber trotzdem kann ich einen String zuweisen und sogar eine Konstante überschreiben. Je nach dem zu welcher JavaScript Version man das dann kompiliert, gibt's dann sogar im Browser keinen Fehler, weil es "const" erst seit ECMAScript 2015 gibt.

TypeScript lässt dich alles machen, was du in reinem JavaScript auch machen kannst. Aber TypeScript versucht dich in 99% der Fälle zumindest daran zu hindern groben Unfug anzustellen. Aber wie in dem Beispiel gezeigt: Wenn man unbedingt will, macht man halt einen Cast zu "any" und hat wieder sämtliche Möglichkeiten (und Probleme) von JavaScript.
 
Zuletzt bearbeitet:
Ich bin davon ausgegangen, dass wenn ich ein => void angebe, ich dann nur eine Methode übergeben kann, die auch nichts zurückliefert. Aber die Methode liefert ja ganz klar einen boolean zurück und trotzdem meckert TS nicht, DAS finde ich merkwürdig.
 
Dann lies doch einfach noch mal, was ich oben geschrieben habe...

Ich versuch's noch mal:
Code:
function fetchResults(callback: (statusCode: number, results: number[]) => void) {
Der Vertrag der Funktionsparameter besagt doch einfach nur: "Ich erwarte hier eine Funktion, der ich 2 Parameter übergeben kann: Eine number und ein number-Array. Und wenn ich diese Funktion aufrufe, interessiert mich der Rückgabewert nicht bzw. ich würde ihn nicht weiter benutzen, falls es einen gäbe."

Steht auch in etwa so in der offiziellen Dokumentation: https://www.typescriptlang.org/docs...o-s-and-don-ts.html#return-types-of-callbacks
"Why: Using void is safer because it prevents you from accidentally using the return value of x in an unchecked way:"

Es geht darum, dass du innerhalb deiner "fetchResults" Funktion sauber mit den Typen arbeiten kannst.

Und wenn du die Typisierung durchziehst, dann hätte jede Variable, die irgendwie mit dem Return Wert von "callback" oder "fetchResults" in Berührung kommt den Typ "void". Und genau deshalb wird dich der TypeScript Compiler daran hindern, diese Variable weiter zu nutzen. Und deswegen ist es vollkommen egal, ob da nun in Wirklichkeit ein boolean drin steht, oder undefined. Solang du dich innerhalb von TypeScript bewegst, wirst du niemals auf diese Variable zugreifen. Und dann passiert auch nichts unerwartetes.
Wenn du dann natürlich irgendwo eine Funktion benutzt, die "any" akzeptiert, bist du selbst schuld. Sowas passiert im Alltag nicht - abgesehen vom Debugging mit console.log und gerade da wirst du niemals auf die Idee kommen eine "void" Variable zu debuggen, weil du sie eh nirgends verwenden kannst, und es deshalb überhaupt keinen Grund gibt, dir den Wert dieser Variable anzeigen zu lassen, weil sie in sauberem TypeScript Code niemals die Ursache eines Fehlers sein kann.

Also mein Fazit: TypeScript meckert in diesem Fall nicht, weil es überhaupt keine Rolle für die Typsicherheit spielt und so gleichzeitig die Flexibilität der möglichen Methoden Parameter erhöht wird.

Btw: Sollte der Compiler deiner Meinung nach auch meckern, weil deine "handler" Funktion nur einen Parameter akzeptiert, obwohl du aber in "fetchResults" diese Funktion mit 2 Parametern aufrufst? Und das alles obwohl "results" in "callback" nicht als optional deklariert wurde.
 
Zuletzt bearbeitet:
Zurück
Oben