L04 · Section 1
Pourquoi Zod ?
Zod est une librairie de validation de schémas TypeScript-first qui fonctionne également en JavaScript pur. Elle permet de valider des données à l'exécution et d'en inférer les types TypeScript automatiquement.
Le problème sans Zod
// Vous recevez des données d'une API — aucune garantie sur la structure
const data = await fetch('/api/user').then(r => r.json());
// Vous supposez que data.name est une string... mais si c'est null ?
const username = data.name.toUpperCase(); // 💥 TypeError potentielle
// Sans validation, les bugs sont silencieux et difficiles à tracer
La solution avec Zod
import { z } from 'zod'; // CommonJS/ESM
// OU via CDN : const { z } = Zod;
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().optional()
});
// Validation explicite avant utilisation
const user = UserSchema.parse(data); // 🛡️ Lance une erreur si invalide
const username = user.name.toUpperCase(); // ✅ Sûr !
Comparaison avec les alternatives
| Librairie | TypeScript | Poids | API |
|---|---|---|---|
| Zod | ✅ natif | ~14KB | Chaînage fluide |
| Joi | ❌ types séparés | ~24KB | Similaire |
| Yup | Partiel | ~18KB | Similaire |
| validator.js | ❌ | ~4KB | Fonctions |
CDN (browser) :
<script src="https://cdn.jsdelivr.net/npm/zod/lib/index.umd.js"></script>
<script>
const { z } = Zod; // Accès au namespace global
</script>
L04 · Section 2
Types primitifs
const { z } = Zod;
// Types de base
z.string() // Toute chaîne de caractères
z.number() // Tout nombre (entier ou flottant)
z.boolean() // true ou false
z.date() // Objet Date JavaScript
z.null() // Exactement null
z.undefined() // Exactement undefined
z.any() // N'importe quelle valeur (pas de validation)
z.unknown() // Valeur inconnue — doit être castée avant usage
z.never() // N'accepte aucune valeur (impossible)
// Literal — valeur exacte
z.literal('admin') // Exactement la string 'admin'
z.literal(42) // Exactement le nombre 42
z.literal(true) // Exactement true
// Enum — liste de valeurs autorisées
const Role = z.enum(['admin', 'user', 'moderator']);
type Role = z.infer<typeof Role>; // 'admin' | 'user' | 'moderator'
Exemples pratiques
// Valider des types simples
z.string().parse('Bonjour'); // ✅ 'Bonjour'
z.number().parse(42); // ✅ 42
z.boolean().parse(true); // ✅ true
z.string().parse(42); // ❌ ZodError: Expected string, received number
// Coercition (conversion automatique)
z.coerce.number().parse('42'); // ✅ 42 (string → number)
z.coerce.date().parse('2024-01-15'); // ✅ Date object
L04 · Section 3
Validateurs
Les validateurs s'appliquent en chaîne sur les types primitifs.
Validateurs string
z.string()
.min(2, 'Minimum 2 caractères')
.max(50, 'Maximum 50 caractères')
.email('Email invalide')
.url('URL invalide')
.regex(/^[a-z]+$/, 'Lettres minuscules seulement')
.startsWith('https://')
.endsWith('.com')
.includes('@')
.length(10) // Exactement 10 chars
.trim() // Supprime les espaces en bordure (transform)
.toLowerCase() // Convertit en minuscules (transform)
Validateurs number
z.number()
.min(0, 'Doit être positif')
.max(100, 'Maximum 100')
.int('Doit être un entier')
.positive() // > 0
.nonnegative() // >= 0
.negative() // < 0
.multipleOf(5) // Multiple de 5
optional, nullable, default
// .optional() — la valeur peut être undefined
const schema = z.string().optional();
schema.parse(undefined); // ✅ undefined
schema.parse('hello'); // ✅ 'hello'
schema.parse(null); // ❌ null n'est pas undefined
// .nullable() — la valeur peut être null
const schema2 = z.string().nullable();
schema2.parse(null); // ✅ null
schema2.parse(undefined); // ❌
// .default(val) — valeur par défaut si undefined
const schema3 = z.string().default('Inconnu');
schema3.parse(undefined); // ✅ 'Inconnu'
// Combinaisons
z.string().optional().nullable() // string | null | undefined
L04 · Section 4
Objets et tableaux
z.object()
const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(2).max(50),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user'),
bio: z.string().max(200).optional(),
});
// Inférence de type TypeScript (en TS)
// type User = z.infer<typeof UserSchema>
// Méthodes sur les objets
UserSchema.strict() // Rejette les propriétés inconnues
UserSchema.partial() // Rend tous les champs optionnels
UserSchema.required() // Rend tous les champs obligatoires
UserSchema.extend({ age: z.number() }) // Ajouter des champs
UserSchema.pick({ name: true, email: true }) // Garder seulement name, email
UserSchema.omit({ bio: true }) // Exclure bio
z.array()
// Tableau de strings
z.array(z.string())
// Tableau d'objets
const TagsSchema = z.array(z.string().min(1)).min(1).max(10);
// Validations sur les tableaux
z.array(z.number())
.min(1, 'Au moins 1 élément')
.max(100, 'Maximum 100 éléments')
.nonempty('Tableau vide interdit')
// Premier élément typé différemment (tuple)
z.tuple([z.string(), z.number()]) // [string, number]
Imbrication
const PostSchema = z.object({
id: z.number(),
title: z.string(),
author: z.object({ // Objet imbriqué
id: z.number(),
name: z.string(),
}),
tags: z.array(z.string()), // Tableau imbriqué
meta: z.record(z.string()), // { [key: string]: string }
});
L04 · Section 5
parse vs safeParse
.parse() — Lance une exception
const UserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
try {
const user = UserSchema.parse({ name: 'A', email: 'invalide' });
} catch (err) {
if (err instanceof ZodError) {
console.log(err.issues);
// [
// { code: 'too_small', path: ['name'], message: 'String must contain at least 2 character(s)' },
// { code: 'invalid_string', path: ['email'], message: 'Invalid email' }
// ]
}
}
.safeParse() — Retourne un résultat
const result = UserSchema.safeParse({ name: 'A', email: 'invalide' });
if (result.success) {
// TypeScript sait que result.data est valide ici
console.log('Données valides :', result.data);
} else {
// result.error est un ZodError
console.log('Erreurs :', result.error.issues);
// Formater les erreurs par champ
const erreurs = {};
result.error.issues.forEach(issue => {
const champ = issue.path.join('.');
erreurs[champ] = issue.message;
});
console.log(erreurs); // { name: '...', email: '...' }
}
Règle : Utilisez
safeParse pour valider les entrées utilisateur
(formulaires, paramètres URL) car vous pouvez afficher les erreurs sans crasher.
Utilisez parse pour des données supposées valides (ex: variables d'environnement).
L04 · Section 6
Transformations & validations avancées
.transform() — Transformer les données
// Normaliser à la validation
const EmailSchema = z.string()
.email()
.transform(val => val.toLowerCase().trim());
EmailSchema.parse(' Alice@Example.COM ');
// ✅ 'alice@example.com'
// Transformer un nombre en string
const StrNumberSchema = z.string().transform(val => parseInt(val, 10));
StrNumberSchema.parse('42'); // ✅ 42
.refine() — Validation personnalisée
// Validation simple
const PasswordSchema = z.string()
.min(8)
.refine(val => /[A-Z]/.test(val), 'Doit contenir une majuscule')
.refine(val => /[0-9]/.test(val), 'Doit contenir un chiffre');
// Validation inter-champs (passwords must match)
const RegisterSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
data => data.password === data.confirmPassword,
{
message: 'Les mots de passe ne correspondent pas',
path: ['confirmPassword'], // Champ sur lequel afficher l'erreur
}
);
z.union() — Types multiples
// Accepter plusieurs types
const IdSchema = z.union([z.string(), z.number()]);
IdSchema.parse('abc-123'); // ✅
IdSchema.parse(42); // ✅
IdSchema.parse(true); // ❌
// Union discriminée (plus performant)
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('login'), userId: z.string() }),
z.object({ type: z.literal('logout'), userId: z.string(), duration: z.number() }),
]);
// Messages d'erreur personnalisés
const NameSchema = z.string({
required_error: 'Le nom est requis',
invalid_type_error: 'Le nom doit être une chaîne',
}).min(2, 'Trop court').max(50, 'Trop long');
✅ Récapitulatif L04
- 🛡️ Types primitifs — string, number, boolean, date, literal, enum
- 🔧 Validateurs — .min(), .max(), .email(), .optional(), .nullable()
- 📦 Structures — z.object(), z.array(), .strict(), .partial(), .extend()
- ⚡ parse vs safeParse — throw vs résultat structuré
- 🔄 transform + refine — normaliser et valider selon logique métier