//

TypeScript 4.5 und der Awaited Type

21.1.2022 | 8 Minuten Lesezeit

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

1import obj from "./something.json" assert { type: "json" };
2

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

Private Field Presence Checks

1class Person {
2   #name: string;
3   constructor(name: string) {
4       this.#name = name;
5   }
6   equals(other: unknown) {
7       return other &&
8           typeof other === "object" &&
9           #name in other && // <- this is new!
10           this.#name === other.#name;
11   }
12}
13

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

1import { Animal } from "./animal.js";
2eval("console.log(new Animal().isDangerous())");
3

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:

1<script setup>
2 import { someFunc } from "./some-module.js";
3</script>
4<button>Click me!</button>
5

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:

1export interface Success {
2  type: `${string}Success`;
3  body: string;
4}
5 
6export function handler(r: Success | Error) {
7  if (r.type === "HttpSuccess") {
8    // (parameter) r: Success
9    const token = r.body;
10  }
11}
12

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:

1async function usingAwaitedAsAReturnType(): Awaited {
2   const result = await Promise.resolve(true)
3   return result;
4}
5

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.

1let a: Promise<string>;
2let b: Promise<Promise<string>>;
3let c: Promise<Promise<Promise<string>>>
4

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:

1let d: Awaited<typeof c>
2

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

1let e : string
2

Nun vergleichen wir die Variablen:

1a===b
2// error always returns false
3// This condition will always return 'false' since the types 'Promise<Promise<string>>' and 'Promise<Promise<Promise<string>>>' have no overlap.ts(2367)
4b===c
5// error always returns false
6

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.

1e===d
2

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

1const awaitA = await a
2const awaitB = await b
3const awaitC = await c
4await a === await b
5await a === await d
6

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:

1declare function MaybePromise<T>(value: T): T | Promise<T> | PromiseLike<T>;
2

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:

1async function doSomething(): Promise<[number, number]> {
2 const result = await Promise.all([MaybePromise(100), MaybePromise(200)]);
3 return result;
4}
5

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

1const result = await Promise.all([Promise.resolve(10), Promise.resolve(20)]);
2

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:

1// Error!
2//
3//    [number | Promise<100>, number | Promise<200>]
4//
5// is not assignable to type
6//
7//    [number, number] 
8

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 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():

1all<T1, T2>(values: readonly [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;
2

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

1all<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>[]>;
2

Hier ist der Return-Wert ein Promise[]>.
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!

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.