TypeScript 4.5 und der Awaited Type

Keine Kommentare

TypeScript 4.5 führt unter anderem einen neuen Utility Type ein: Awaited.
In diesem Blogpost möchte ich erklären, was dieser tut, welche Probleme er löst und wo er für uns nützlich sein kann. Stefan Spittank und ich haben zu diesem Thema auf unserem Youtube-Kanal Papperlapapp auch ein Video zu dem Thema aufgenommen. Schaut doch auch mal da rein.

Falls ihr mitprogrammieren wollt: Ihr findet hier meine Beispiel-Sourcen mit den Experimenten mit dem Awaited Type.
Ich habe für das Wechseln zwischen zwei TypeScript-Versionen die VS-Code-Funktionalität zum Wechseln der TypeScript-Version verwendet.

TypeScript 4.5

TypeScript 4.5 erschien im November 2021. Eine neue Minor-Version von TypeScript löst bei mir normalerweise keine Begeisterungsstürme aus. Auch die Features lesen sich nett, aber sie sind für mich jetzt keine Revolution.
Allerdings gibt es da ein Feature, das mich neugierig gemacht hat: der Awaited Type! Interessant auch, weil ich die Intention auf den ersten (und zweiten Blick) nicht verstanden habe. Aber bevor wir uns den Awaited Type anschauen, werfen wir doch einen schnellen Blick auf einige weitere Änderungen:

Import Assertions

Ich kann nun beim Import den Typ der Datei überprüfen. Praktisch, aber für mich nicht weltbewegend.

Private Field Presence Checks

Ich kann nun mit dem in-Operator überprüfen, ob es auf einem Object eine private Property gibt. Private Property meint hier: ECMA-Script private Property #, nicht das TypeScript private-Keyword.
Auch nett. Ich arbeite allerdings recht selten mit Klassen, dies habe ich noch nicht vermisst.

Disabling Import Elision

Ich kann nun mit dem Compiler-Flag preserveValueImports verhindern, dass solcherlei Imports von TypeScript entfernt werden, da sie ja anscheinend nicht benutzt werden. Klingt erstmal unnötig, allerdings kann dies für Vue- oder Svelte-Entwicklung durchaus praktisch sein.

Zum Beispiel für diesen Vue-Code hier:

Template String Types as Discriminants

TypeScript 4.5 kann nun auch Werte innerhalb eines Template Strings auswerten und Rückschlüsse auf den Typ ziehen:

Das ist alles irgendwie ganz praktisch, hat für mich aber nicht so große praktische Auswirkungen!
Aber irgendetwas hat dann doch meine Aufmerksamkeit geweckt.

Der Awaited Typ

Die Dokumentation beschreibt einen Awaited Typ!
Ich habe mich schon länger mit Async/Await beschäftigt.
Ich finde dass dies ein spannendes, allgegenwärtiges Thema ist, in dem aus meiner Sicht immer noch genug Fallstricke lauern.
Und jetzt kommt da Compiler-Unterstützung! Ich war begeistert!
In meinem Kopf wurde schon durch Type Inference für immer das Problem des vergessenen Await gelöst. Das klingt wunderbar. Dann klingelte der Wecker … oder so ähnlich.

Der Awaited Typ ist super! Aber er löst eigentlich eine ganz andere Art Problem, wenn auch eine verwandte Art Problem.

Aber fangen wir mal langsam an:

Meine erste Begeisterung habe ich sofort in Code gegossen:

Awaited ist natürlich der Return Type einer async-Function! Richtig? Falsch!

Der Compiler sagt dazu:

The return type of an async function or method must be the global Promise type. Did you mean to write 'Promise'?ts(1064)

Meh! Leicht enttäuscht habe ich mir dann widerwillig erstmal die Dokumentation angeschaut. Was macht Awaited überhaupt?

This type is meant to model operations like await in async functions, or the .then() method on Promises – specifically, the way that they recursively unwrap Promises.

Ah! Es geht also eher um den „await”-Teil als um den „async” Teil von async/await!
Recursively Unwrap Promises! Moment! Gibt es überhaupt verschachtelte Promises in JavaScript? Also zumindest nicht zur Laufzeit! Was soll das Ganze dann? Scheinbar gibt es Situationen, in denen das TypeScript-Typsystem nicht erkennen kann, welcher Typ in verschachtelten Promises steckt. Interessant!

Ausprobieren!

Wir erzeugen uns mal drei Promises-Typen mit unterschiedlichem Verschachtelungsgrat. Auf Typsystem-Ebene geht das ja.

Dazu gibt es eine Variable mit dem neuen Awaited Type. Awaited ist ein generischer Typ. Wir geben mal den Typen von Variable c dort hinein:

Und als letztes noch eine schnöde Variable vom Typ string

Nun vergleichen wir die Variablen:

Wenn wir a, b und c vergleichen, sagt uns der Compiler schon: Die Variablen können gar nicht gleich sein!
Wenn wir dann e und d vergleichen, sagt uns der Compiler: Das sind beides Variablen vom Typ string. Spannend! Die könnten also zur Laufzeit === sein.

Schauen wir uns den gleichen Vergleich von oben mithilfe der Auflösung durch await an, so ist der Compiler im Gegensatz zu oben zufrieden.

Das heißt für mich an dieser Stelle, dass Awaited den Typen abbildet, der nach einem await aufgelöst wird. Klingt irgendwie sinnvoll! Aber wofür braucht man das?

Das Beispiel erklärt

Schauen wir uns doch mal das Beispiel aus der Dokumentation an. Das hilft sicher weiter.

Es wird eingeleitet mit:

The Awaited type can be helpful for modeling existing APIs, including JavaScript built-ins like Promise.all, Promise.race, etc.

Es geht also auch darum, 3rd Party Code, bzw. Code, den wir nicht unter Kontrolle haben, besser zu typisieren. Mal im Hinterkopf behalten.

Also:

Das Beispiel fängt an mit einer Typdefinition:

Uff! Also wir erklären dem Compiler, dass irgendwo die Funktion namens MaybePromise existiert. Das ist eine generische Funktion, die mit einem Typen T (z. B. string) genauer bestimmt werden kann. Diese Methode nimmt einen Wert dieses Typs T entgegen. Und als Rückgabewert liefert diese Funktion entweder einen Wert des Typs T, ein Promise, das durch den T resolved wird oder ein PromiseLike, das mit einem Wert des Typ T resolved wird.
Puh! Ich habe Fragen!
Zunächst mal: Was ist das für eine seltsame Funktion? Mit noch eigensinnigerem Rückgabewert! So etwas ist mir noch nie begegnet.
Ich werde später sehen: Oh doch! 🙂
Und was ist ein PromiseLike? Das ist nur am Rande wichtig für unser Beispiel. Also nur soviel: Ein PromiseLike ist eine Datenstruktur, welche eine then-Funktion besitzt. Es ist ein Überbleibsel aus den Promise-Anfangstagen. Als es verschiedene, ähnliche Promise-Implementierungen gab, wie z. B. Q.

Also wir haben eine Funktion, die entweder einen konkreten Wert oder ein Promise dieses konkreten Werts liefert. Gut!

Weiter im Beispiel:

Wir definieren eine asynchrone Funktion, die ein Promise liefert, welches ein number-Array enthält. Gut, async/await Methoden liefern ihre Werte immer gewrapped in einem Promise. So weit, so gut.
Die erste Zeile lässt klar werden, warum wir es hier mit einem number-Array zu tun haben:
Wir verwenden hier ein Promise.all. Wir erinnern uns:
Ein Promise.all() liefert ein Promise mit einem Array mit allen Werten oder ein rejectetes Promise, falls ein Promise rejected.
Wir verwenden das Promise.all allerdings nicht mit „normalen” Promises, sondern mit unserer Funktion von oben: MaybePromise() mit dem number Typ.
„Normalerweise” würden wir Promise.all() in der Art

aufrufen.
Das Ergebnis wäre dann ein Promise mit den Array [10,20]. Hier ist der Eintrag in unserem Array irgendein noch nicht definierter Wert: Entweder eine number, ein Promise mit dieser number oder ein PromiseLike mit dieser number.

Als letztes geben wir unseren Wert einfach zurück. Das sieht doch gut aus! Das sollte doch kompilieren!
Also hätte ich jetzt gesagt. Und damit hatte ich auch fast Recht…

TypeScript 4.4 vs. 4.5

Aber schauen wir mal genauer drauf.
Wenn wir das eingangs erwähnte Beispiel mit TypeScript 4.5 ausprobieren, klappt der einfach! Super!
Mit TypeScript 4.4 sehen wir hier allerdings einen Compilefehler:

Und siehe da! Der Compiler teilt uns mit, dass er sich nicht ganz sicher ist, von welchem Typ das Ergebnis sein wird, bzw. welchen Typ ein Element unseres Arrays haben wird:
number oder Promise.
Wir als alte async/await-Hasen sagen sofort: „Ja komm: await Promise<number> ist doch ne number.”
Stimmt! Und genau dafür ist der Awaited Type!

Aber Moment! Ich sehe hier keinen Awaited Typ! Da bin ich anfangs auch drüber gestolpert.
Die schlauen TypeScript-Entwickler haben in TypeScript 4.4 und TypeScript 4.5 die Signatur von unter anderem Promise.all geändert.
Schauen wir mal rein:

TypeScript 4.4

In TypeScript 4.4 ist die Signatur von Promise.all():

Sieht doch erstmal gut aus: Fast! Und ah! Da sehen wir auch wieder unser PromiseLike wieder! Aber schauen wir uns mal den Return Typ an:
Promise<[T1, T2]>
Es kommt ein Promise mit einem Array von Werten zurück. Super!
Das sind allerdings generische Werte. „Normalerweise” ein string oder ein Object.
In unserem Beispiel allerdings ist unser T1 und unser T2 dieser Weiß-ich-nicht-Typ: Könnte eine number sein, vielleicht aber auch ein Promise.
Und genau das bildet das Typsystem in TypeScript 4.4 hier noch nicht ab. Also der Compilefehler!

Wie wurde das jetzt gelöst?
Schauen wir rein:

TypeScript 4.5

Hier ist der Return-Wert ein Promise<Awaited<T>[]>.
Also fast so wie oben, nur, dass wir mit Awaited ausdrücken, dass wir auf jeden Fall hier ein aufgelöstes Promise haben werden! (Falls ein Promise rejected ist das ganze Promise rejected).
Und schon klappt das! Elegant!

Fazit

Puh! Also der Awaited Type kann also Promises entschachteln.
Wofür brauche ich das jetzt?
Für mich bedeutet Awaited, dass die Arbeit mit externen Abhängigkeiten, wie z. B. sogar auch der JavaScript Runtime, in Grenzbereichen angenehmer wird. Der Compiler verhält sich mehr wie ich es erwarte. Die Funktionalität ist hilfreich, wenn ich mit Bibliotheken arbeite, die ich nicht unter Kontrolle habe, aber eigentlich ist dies etwas, das die Lib schon gemacht haben sollte.
Letztendlich war die Motivation für diesen Typ ja auch genau das Auftreten dieser Schwierigkeiten mit Promise.all().
Wo wende ich selbst diesen Typ an? Ich glaube momentan: Gar nicht! Wenn ich selbst eine Bibliothek bereitstelle, dann ist das hilfreich, um das Konsumieren einfacher zu machen. Oder aber wenn ich im Code aus Gründen mit verschachtelten Promises zu tun habe.

Also nehme ich einfach mit: Die Arbeit mit Promises in TypeScript ist noch ein Stückchen angenehmer geworden, ohne dass ich das vielleicht direkt sehe. Und das finde ich super!

Habt ihr Ideen, für was ihr den Awaited Type einsetzen wollt? Wart ihr genauso gehyped wie ich? Habt ihr ähnlich wie ich an dem Beispiel geknabbert? Schreibt mir doch einen Kommentar!

Holger ist seit 2015 Senior Consultant und Senior Developer bei der codecentric. Er ist seit jeher fasziniert vom Web und den damit verbundenen Möglichkeiten. Besonders die rasante Entwicklung der Technologien im Frontend verfolgt und begleitet er mit Begeisterung. Aber auch das Backend lässt ihn nie ganz los: sei Node, oder Kotlin. Gute Testbarkeit ist Holger bei der Entwicklung und dem Entwurf von Software ein besonderes Anliegen. Die Gedanken des Software Craftings spielen dabei eine große Rolle. In seiner Freizeit podcastet er bei @autoweird.fm und produziert Videos auf dem Youtube-Kanal Papperlapapp.

Über 1.000 Abonnenten sind up to date!

Die neuesten Tipps, Tricks, Tools und Technologien.
Jede Woche direkt in deine Inbox.

Kostenfrei anmelden und immer auf dem neuesten Stand bleiben!
(Keine Sorge, du kannst dich jederzeit abmelden.)

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht.