TW3 — Technologies du Web 3

Atelier — Typer un client API

Convertir un script JavaScript non typé en TypeScript strict, puis typer complètement un client d'API (réponse, erreurs, pagination) avec des type guards.

Cet atelier met en pratique les acquis des 4 pages Apport sur un cas concret : typer un petit client d'API. Vous convertissez d'abord du JavaScript non typé, puis vous construisez une fonction typée de bout en bout.

Le point de départ : un script JavaScript

Voici un script JavaScript courant — une fonction qui charge un utilisateur et ses posts. Il « fonctionne » mais aucune erreur n'est capturée à l'écriture :

// JavaScript non typé : tout est permis jusqu'au crash en prod
function chargerUtilisateur(id) {
  return fetch(`/api/users/${id}`)
    .then((res) => res.json())
    .then((user) => {
      return fetch(`/api/users/${user.id}/posts`)
        .then((res) => res.json())
        .then((posts) => ({ utilisateur: user, posts }))
    })
}

// Ces appels ne déclenchent AUCUNE erreur :
chargerUtilisateur("abc")   // → plante en prod (user.id sur un string ?)
chargerUtilisateur(null)    // → plante en prod

Étape 1 : Activer le mode strict

npm install -D typescript @types/node
npx tsc --init

Dans tsconfig.json, activez le mode strict :

{
  "compilerOptions": {
    "strict": true,
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "Node",
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

Lancez npx tsc --noEmit sur le script ci-dessus renvoie des erreurs : les paramètres ne sont pas typés, les réponses fetch sont any, et les promesses n'ont pas de type de retour.

Étape 2 : Décrire les données avec les types

Créez les interfaces qui décrivent ce que vous manipulez :

interface Post {
  id: number
  titre: string
  contenu: string
}

interface User {
  id: number
  nom: string
  email: string
}

interface PagePosts {
  page: number
  total: number
  resultats: Post[]
}

Décrire d'abord les données, puis le code. C'est la pratique centrale de TypeScript : les types guident l'implémentation.

Étape 3 : Typer la fonction et le type guard

Ajoutez le type guard pour valider la réponse API, puis la fonction chargerUtilisateur :

interface Post {
  id: number
  titre: string
  contenu: string
}

interface User {
  id: number
  nom: string
  email: string
}

interface PagePosts {
  page: number
  total: number
  resultats: Post[]
}

function estUser(v: unknown): v is User {
  return (
    typeof v === "object" &&
    v !== null &&
    "id" in v &&
    typeof (v as User).id === "number" &&
    "nom" in v &&
    typeof (v as User).nom === "string" &&
    "email" in v &&
    typeof (v as User).email === "string"
  )
}

function estPagePosts(v: unknown): v is PagePosts {
  return (
    typeof v === "object" &&
    v !== null &&
    "page" in v &&
    typeof (v as PagePosts).page === "number" &&
    "total" in v &&
    typeof (v as PagePosts).total === "number" &&
    "resultats" in v &&
    Array.isArray((v as PagePosts).resultats)
  )
}

async function chargerUtilisateur(id: number): Promise<{ utilisateur: User; posts: PagePosts }> {
  const resUser = await fetch(`/api/users/${id}`)
  if (!resUser.ok) {
    throw new Error(`HTTP ${resUser.status}`)
  }
  const dataUser: unknown = await resUser.json()
  if (!estUser(dataUser)) {
    throw new Error("Format utilisateur invalide")
  }

  const resPosts = await fetch(`/api/users/${dataUser.id}/posts`)
  if (!resPosts.ok) {
    throw new Error(`HTTP ${resPosts.status}`)
  }
  const dataPosts: unknown = await resPosts.json()
  if (!estPagePosts(dataPosts)) {
    throw new Error("Format posts invalide")
  }

  return { utilisateur: dataUser, posts: dataPosts }
}

Étape 4 : Gérer les erreurs proprement

Avec unknown dans le catch, TypeScript oblige à prouver le type de l'erreur :

interface User {
  id: number
  nom: string
  email: string
}

interface PagePosts {
  page: number
  total: number
  resultats: Post[]
}

interface Post {
  id: number
  titre: string
  contenu: string
}

function estUser(v: unknown): v is User {
  return (
    typeof v === "object" &&
    v !== null &&
    "id" in v &&
    typeof (v as User).id === "number" &&
    "nom" in v &&
    typeof (v as User).nom === "string" &&
    "email" in v &&
    typeof (v as User).email === "string"
  )
}

function estPagePosts(v: unknown): v is PagePosts {
  return (
    typeof v === "object" &&
    v !== null &&
    "page" in v &&
    typeof (v as PagePosts).page === "number" &&
    "total" in v &&
    typeof (v as PagePosts).total === "number" &&
    "resultats" in v &&
    Array.isArray((v as PagePosts).resultats)
  )
}

async function chargerUtilisateur(id: number): Promise<{ utilisateur: User; posts: PagePosts }> {
  const resUser = await fetch(`/api/users/${id}`)
  if (!resUser.ok) throw new Error(`HTTP ${resUser.status}`)
  const dataUser: unknown = await resUser.json()
  if (!estUser(dataUser)) throw new Error("Format utilisateur invalide")
  const resPosts = await fetch(`/api/users/${dataUser.id}/posts`)
  if (!resPosts.ok) throw new Error(`HTTP ${resPosts.status}`)
  const dataPosts: unknown = await resPosts.json()
  if (!estPagePosts(dataPosts)) throw new Error("Format posts invalide")
  return { utilisateur: dataUser, posts: dataPosts }
}

// Gestion d'erreur typée :
async function main(): Promise<void> {
  try {
    const { utilisateur, posts } = await chargerUtilisateur(1)
    console.log(utilisateur.nom)
    console.log(posts.resultats.length)
  } catch (err) {
    // err est unknown — il faut restreindre
    if (err instanceof Error) {
      console.error(err.message)
    } else {
      console.error("Erreur inconnue", err)
    }
  }
}

Étape 5 : Améliorer avec le motif Result

Pour éviter les exceptions non documentées, exposez un résultat explicite :

interface Post {
  id: number
  titre: string
  contenu: string
}

interface User {
  id: number
  nom: string
  email: string
}

interface PagePosts {
  page: number
  total: number
  resultats: Post[]
}

type Result<T, E = string> =
  | { ok: true; valeur: T }
  | { ok: false; erreur: E }

function estUser(v: unknown): v is User {
  return (
    typeof v === "object" &&
    v !== null &&
    "id" in v &&
    typeof (v as User).id === "number" &&
    "nom" in v &&
    typeof (v as User).nom === "string" &&
    "email" in v &&
    typeof (v as User).email === "string"
  )
}

function estPagePosts(v: unknown): v is PagePosts {
  return (
    typeof v === "object" &&
    v !== null &&
    "page" in v &&
    typeof (v as PagePosts).page === "number" &&
    "total" in v &&
    typeof (v as PagePosts).total === "number" &&
    "resultats" in v &&
    Array.isArray((v as PagePosts).resultats)
  )
}

async function chargerUtilisateur(id: number): Promise<Result<{ utilisateur: User; posts: PagePosts }>> {
  try {
    const resUser = await fetch(`/api/users/${id}`)
    if (!resUser.ok) return { ok: false, erreur: `HTTP ${resUser.status}` }
    const dataUser: unknown = await resUser.json()
    if (!estUser(dataUser)) return { ok: false, erreur: "Format invalide" }

    const resPosts = await fetch(`/api/users/${dataUser.id}/posts`)
    if (!resPosts.ok) return { ok: false, erreur: `HTTP ${resPosts.status}` }
    const dataPosts: unknown = await resPosts.json()
    if (!estPagePosts(dataPosts)) return { ok: false, erreur: "Format invalide" }

    return { ok: true, valeur: { utilisateur: dataUser, posts: dataPosts } }
  } catch (err) {
    return { ok: false, erreur: err instanceof Error ? err.message : "Erreur inconnue" }
  }
}

// L'appelant DOIT traiter l'erreur :
async function main(): Promise<void> {
  const result = await chargerUtilisateur(1)
  if (result.ok) {
    console.log(result.valeur.utilisateur.nom)
    console.log(result.valeur.posts.resultats.length)
  } else {
    console.error(result.erreur)
  }
}

📋 Récapitulatif de l'atelier

ConceptOù dans le code
interface pour décrire les donnéesUser, Post, PagePosts
Promise<T> pour les fonctions asynchroneschargerUtilisateur retourne Promise<Result<...>>
Type guard v is T pour validerestUser, estPagePosts
[unknown](/typescript/primitifs-inference#unknown-vs-any) + narrowing dans le catchGestion d'erreur typée
Motif Result pour forcer le traitement{ ok: true, valeur } | { ok: false, erreur }

🎯 Exercice final de l'atelier

Ajoutez la pagination à chargerUtilisateur : le paramètre page: number est optionnel (valeur par défaut 1), et la fonction retourne Promise<Result<PagePosts>> directement.

→ Passez à l'exercice évalué quand vous êtes prêt.

TypeScript

Terminez le quiz du chapitre pour le marquer comme complété.

Tableau de bord

On this page