Featured image

Series: TypeScript

Awaited Link to heading

Introduzione Link to heading

La versione 4.5 di TypeScript ha introdotto la type function Awaited. Il suo scopo è quello di aiutarci a modellare meglio tutte quelle situazioni in cui è implicato un await, o comunque il metodo then delle promise. La situazione è meno banale di quanto si potrebbe pensare, in quanto il JavaScript “unwrappa” automaticamente eventuali promise innestate, una espressione come await 42 è perfettamente lecita e vi è il supporto ai thenable, ovvero plain objects che dichiarano il metodo then.

Ecco che la type function Awaited è lo strumento adeguato per tutte queste casistiche:

type test1 = Awaited<42> // 42
type test2 = Awaited<Promise<number>> // number
type test3 = Awaited<Promise<Promise<string>>> // string
type test4 = Awaited<{ then: (onfulfilled: (v: string[]) => void) => void }> // string[]

Playground

Gli internals Link to heading

La definizione di Awaited è la seguente:

type Awaited<T> =
    T extends null | undefined ? T :
        T extends object & { then(onfulfilled: infer F): any } ? 
            F extends ((value: infer V, ...args: any) => any) ? 
                Awaited<V> :
                never :
        T;

Il primo controllo che viene eseguito riguarda i tipi null e undefined: se il flag --strictNullChecks non fosse attivo allora entrambi sarebbero sottotipi di qualunque tipo, e ciò causerebbe problemi negli step successivi. In particolare, il tipo risultante sarebbe never anziché null oppure undefined.

In secondo luogo si controlla se il tipo T non è un primitivo ed è un thenable, e ricordiamo che le promise stesse sono dei thenable. Per essere un thenable il metodo then deve dichiarare un parametro, onfulfilled, il quale deve essere una callback da invocare con un qualsiasi valore, value. Tale valore è a tutti gli effetti il valore di completamento del thenable. La keyword infer permette prima di estrarre il tipo di onfulfilled e poi quello di value, il quale verrà “unwrappato” ricorsivamente.

Quali sono i casi base della ricorsione? Il primo, null | undefined, l’abbiamo già discusso. Inoltre, se il tipo T corrisponde ad un tipo primitivo o a un oggetto che non è un thenable, tale tipo T verrà direttamente restituito. Infine, se il tipo T corrisponde ad un thenable il cui parametro non è invocabile abbiamo a che fare con un thenable che non si completerà mai, quindi il giusto tipo risultante è never.

Promise.all Link to heading

Un caso d’uso della type function Awaited è il metodo statico all della classe Promise:

interface PromiseConstructor {
  all<T extends readonly unknown[] | []>(values: T): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
}

L’array o la tupla presi come input possono contenere, da specifica, qualunque valore. Il risultato sarà una promise contenente un array, o una tupla, mutabile dove il tipo di ogni cella è pari al tipo della corrispondente cella dell’input dato in pasto proprio alla Awaited.