TSTypeScript · Lesson 8 of 10

Async TypeScript

TypeScript's async types are straightforward once you know the pattern: async functions return `Promise<T>`, and `await` unwraps them to `T`. The tricky part is typing errors, which TypeScript forces you to acknowledge.

TypeScript
// async functions always return Promise<T>
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json() as Promise<User>
}

// Promise<void> for functions that don't return a value
async function logUser(id: number): Promise<void> {
  const user = await fetchUser(id)
  console.log(user.name)
}

// Promise.all — parallel with typed results
async function fetchMultiple(ids: number[]): Promise<User[]> {
  return Promise.all(ids.map(id => fetchUser(id)))
}

// Promise.allSettled — partial success
async function fetchSafe(ids: number[]): Promise<(User | null)[]> {
  const results = await Promise.allSettled(ids.map(id => fetchUser(id)))
  return results.map(r => r.status === "fulfilled" ? r.value : null)
}

// Generic fetch wrapper with proper typing
async function apiFetch<T>(url: string): Promise<T> {
  const res = await fetch(url)
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`)
  return res.json()
}

// Usage — TypeScript knows the return type
const users = await apiFetch<User[]>("/api/users")
const post  = await apiFetch<Post>("/api/posts/1")

interface User { id: number; name: string; email: string }
interface Post { id: number; title: string; body: string }
TypeScript
// Result type pattern — explicit error handling without throwing
type Result<T, E = Error> =
  | { ok: true;  value: T }
  | { ok: false; error: E }

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url)
    if (!res.ok) return { ok: false, error: new Error(`HTTP ${res.status}`) }
    const value = await res.json() as T
    return { ok: true, value }
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err : new Error(String(err)) }
  }
}

// Usage
const result = await safeFetch<User[]>("/api/users")
if (result.ok) {
  result.value.forEach(u => console.log(u.name))
} else {
  console.error("Failed:", result.error.message)
}

// Typed EventEmitter pattern
type EventMap = {
  connect:    [userId: number]
  disconnect: [userId: number, reason: string]
  message:    [from: number, to: number, text: string]
}

class TypedEmitter<Events extends Record<string, unknown[]>> {
  private handlers = new Map<keyof Events, ((...args: unknown[]) => void)[]>()

  on<K extends keyof Events>(event: K, fn: (...args: Events[K]) => void): this {
    const list = this.handlers.get(event) ?? []
    this.handlers.set(event, [...list, fn as never])
    return this
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.handlers.get(event)?.forEach(fn => fn(...args))
  }
}
◆ Note
In TypeScript, `catch (err)` gives you `unknown`, not `Error` — because anything can be thrown. Always check `err instanceof Error` before accessing `.message` or `.stack`. The Result<T> pattern above avoids this entirely by keeping errors in the return value.