13 - Typer l'asynchrone : Promise, Awaited et AsyncGenerator
Ce que tu vas apprendre
- Comment
Promise<T>propage les types à travers lesthenetawait Awaited<T>pour deballer les Promises imbriquees- Le problème du
catchqui est toujoursunknown - Les generateurs async :
AsyncGenerator<T>etAsyncIterable<T>
Prerequisites
Avoir lu les articles sur les generics et les conditional types.
Promise : le generic le plus utilise
Chaque fonction async retourne une Promise<T> ou T est le type de la valeur résolue :
typescriptasync function getUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`)
return res.json()
}
const user = await getUser("123") // type: User
TypeScript infere le type de retour d'une fonction async à partir des return :
typescriptasync function fetchData() {
const res = await fetch("/api/data")
const json = await res.json()
return json as { items: string[] }
}
// type infere : Promise<{ items: string[] }>
Promise.all, Promise.race, Promise.allSettled
TypeScript type correctement les combinateurs de Promises :
typescriptconst [user, orders, settings] = await Promise.all([
getUser("123"),
getOrders("123"),
getSettings("123")
])
// user: User, orders: Order[], settings: Settings
const fastest = await Promise.race([
fetchFromCDN(url),
fetchFromOrigin(url)
])
// type: Response (le premier qui resolve)
const results = await Promise.allSettled([
riskyOperation1(),
riskyOperation2()
])
// type: [PromiseSettledResult<A>, PromiseSettledResult<B>]
Promise.allSettled retourne des PromiseSettledResult<T> qui sont des discriminated unions :
typescripttype PromiseSettledResult<T> =
| { status: "fulfilled"; value: T }
| { status: "rejected"; reason: any }
Tu peux filtrer les succes :
typescriptconst fulfilled = results
.filter((r): r is PromiseFulfilledResult<string> => r.status === "fulfilled")
.map(r => r.value)
Awaited
Awaited<T> deballe recursivement les Promises :
typescripttype A = Awaited<Promise<string>> // string
type B = Awaited<Promise<Promise<number>>> // number
type C = Awaited<string> // string (pas une Promise)
type D = Awaited<Promise<string> | number> // string | number
C'est utile pour typer le résultat de await sur des types génériques :
typescriptasync function unwrap<T>(promise: T): Promise<Awaited<T>> {
return await promise
}
Et pour extraire le type résolu d'une fonction async :
typescripttype UserData = Awaited<ReturnType<typeof getUser>> // User
Le problème du catch
En JavaScript, n'importe quoi peut etre lance : une Error, un string, un number, null. TypeScript ne peut pas savoir ce que throw lance dans une lib tierce.
typescripttry {
await riskyOperation()
} catch (err) {
// Avec strict: true, err est unknown
// Tu DOIS verifier le type avant d'utiliser
if (err instanceof Error) {
console.log(err.message) // ✅
}
}
Avant TypeScript 4.4 avec useUnknownInCatchVariables, err etait any par défaut. Maintenant avec strict: true, c'est unknown. C'est mieux mais ca force du boilerplate dans chaque catch.
Un pattern que j'utilise sur paltemps.fr pour simplifier :
typescriptfunction toError(err: unknown): Error {
if (err instanceof Error) return err
if (typeof err === "string") return new Error(err)
return new Error(`Unknown error: ${JSON.stringify(err)}`)
}
try {
await riskyOperation()
} catch (err) {
const error = toError(err)
console.log(error.message) // ✅ toujours safe
}
Le pattern Result pour éviter try/catch
Plutot que de lancer des exceptions, retourne un résultat type (vu dans l'article sur les discriminated unions) :
typescripttype Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
async function safeGetUser(id: string): Promise<Result<User>> {
try {
const user = await getUser(id)
return { ok: true, value: user }
} catch (err) {
return { ok: false, error: toError(err) }
}
}
const result = await safeGetUser("123")
if (result.ok) {
console.log(result.value.name) // ✅ type: User
} else {
console.log(result.error.message) // ✅ type: Error
}
L'erreur est dans le type de retour, pas dans le flux d'exécution. L'appelant est force de gerer les deux cas.
AsyncGenerator et AsyncIterable
Les generateurs async produisent des valeurs une par une, de manière asynchrone :
typescriptasync function* paginate<T>(
fetchPage: (page: number) => Promise<T[]>
): AsyncGenerator<T> {
let page = 1
while (true) {
const items = await fetchPage(page)
if (items.length === 0) break
for (const item of items) {
yield item
}
page++
}
}
// Utilisation
for await (const user of paginate(fetchUsers)) {
console.log(user.name)
}
Le type AsyncGenerator<T> a trois paramètres génériques : AsyncGenerator<Yield, Return, Next>. En pratique, seul Yield est utilise.
AsyncIterable
AsyncIterable<T> est l'interface que for await...of attend. AsyncGenerator implemente AsyncIterable.
typescriptasync function processStream(stream: AsyncIterable<Uint8Array>) {
for await (const chunk of stream) {
process(chunk)
}
}
C'est le type a utiliser dans les signatures de fonctions — plus générique que AsyncGenerator.
Typer un stream d'événements
typescriptasync function* eventStream(url: string): AsyncGenerator<ServerEvent> {
const source = new EventSource(url)
const queue: ServerEvent[] = []
let resolve: (() => void) | null = null
source.onmessage = (e) => {
queue.push(JSON.parse(e.data))
resolve?.()
}
while (true) {
if (queue.length === 0) {
await new Promise<void>(r => { resolve = r })
}
yield queue.shift()!
}
}
for await (const event of eventStream("/api/events")) {
match(event)
.with({ type: "notification" }, handleNotification)
.with({ type: "update" }, handleUpdate)
.exhaustive()
}
Typer les callbacks async
Les callbacks async retournent Promise<T> :
typescripttype AsyncCallback<T> = () => Promise<T>
async function retry<T>(fn: AsyncCallback<T>, maxRetries: number): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (err) {
if (i === maxRetries - 1) throw err
}
}
throw new Error("Unreachable")
}
const user = await retry(() => getUser("123"), 3) // type: User
Le generic T propage le type du callback à travers retry.
Résumé
Promise<T>propage les types à traversawait,then, et les combinateurs (all, race, allSettled)Awaited<T>deballe les Promises recursivement — utile avecReturnType- Le
catchest toujoursunknownavecstrict: true— utilise une fonctiontoErrorou le pattern Result AsyncGenerator<T>produit des valeurs asynchrones une par une — utiliseAsyncIterable<T>dans les signatures- Le pattern Result remplace try/catch par un retour type qui force la gestion des erreurs
Article précédent : 12 - Pattern matching
Article suivant : 14 - Type erasure : ce que TypeScript efface au runtime