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 --initDans 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
| Concept | Où dans le code |
|---|---|
interface pour décrire les données | User, Post, PagePosts |
Promise<T> pour les fonctions asynchrones | chargerUtilisateur retourne Promise<Result<...>> |
Type guard v is T pour valider | estUser, estPagePosts |
[unknown](/typescript/primitifs-inference#unknown-vs-any) + narrowing dans le catch | Gestion 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é.
Asynchrone typé
Typer les promesses avec Promise<T>, comprendre async/await, capturer les erreurs en unknown et typer les réponses fetch.
Exercice — Validation typée d'une réponse API
Validez une donnée inconnue (unknown) reçue d'une API en la transformant en type User sûr, en appliquant le narrowing et les type guards.