React - Types et Validation avec Zod & TypeScript
Salut ! Maintenant qu'on sait faire des formulaires, parlons de la validation des données
Le Problème avec la Validation
TypeScript, c'est bien mais
TypeScript te protège pendant le développement, mais :
// ✅ TypeScript valide ça au build
interface User {
name: string
age: number
email: string
}
function processUser(user: User) {
// TypeScript sait que user.name est un string
console.log(user.name.toUpperCase())
}
// ❌ Mais ça peut arriver à runtime !
const userFromAPI = JSON.parse(response) // any 😱
processUser(userFromAPI) // BOOM si userFromAPI n'a pas la bonne structure !
Les Dangers du Runtime
- 🌐 APIs externes : peuvent renvoyer n'importe quoi
- 📝 Formulaires utilisateur : données non fiables
- 🏪 LocalStorage/SessionStorage : peut être corrompu
- 🔗 URL parameters : manipulables côté client
La Solution ? Validation Runtime
Il nous faut valider les données à l'exécution ET avoir la sécurité des types TypeScript !
Zod - Le Saint Graal ! ⚡
Installation
npm install zod
Philosophie de Zod
- Schema-first : tu définis le schéma une fois
- Type inference : TypeScript types générés automatiquement
- Runtime validation : validation à l'exécution
- Developer-friendly : messages d'erreur clairs
Premier Exemple
import { z } from 'zod'
// 🔥 Définis le schéma une fois
const UserSchema = z.object({
name: z.string().min(1, 'Nom requis'),
age: z.number().min(0).max(120),
email: z.string().email('Email invalide'),
})
// ✅ Type TypeScript généré automatiquement !
type User = z.infer<typeof UserSchema>
// type User = { name: string; age: number; email: string }
// 🛡️ Validation runtime
function createUser(data: unknown): User {
// Ça peut throw une ZodError si invalid
return UserSchema.parse(data)
}
// 🚀 Utilisation
try {
const user = createUser({
name: "Andy",
age: 25,
email: "andy@example.com"
})
console.log(user.name) // TypeScript sait que c'est un string !
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.issues) // Détails des erreurs
}
}
Validation de Données API
Problème Classique
// ❌ Code dangereux qu'on voit partout
async function getUser(id: string) {
const response = await fetch(`/api/users/${id}`)
const user = await response.json() // any - DANGER !
// Qu'est-ce qui se passe si l'API renvoie autre chose ?
return user.name.toUpperCase() // PEUT CRASH !
}
Solution Zod
import { z } from 'zod'
// Schéma de validation
const UserApiSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
avatar: z.string().url().optional(),
createdAt: z.string().datetime(),
settings: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean()
})
})
type UserApi = z.infer<typeof UserApiSchema>
async function getUser(id: string): Promise<UserApi> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('API Error')
const data = await response.json()
// 🛡️ Validation des données de l'API
const user = UserApiSchema.parse(data)
return user // 100% sûr maintenant !
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Invalid API response:', error.issues)
}
throw error
}
}
// 🚀 Utilisation sûre
const user = await getUser('123')
console.log(user.name.toUpperCase()) // ✅ Sûr !
console.log(user.settings.theme) // ✅ TypeScript autocomplete parfait
Validation de Formulaires Avancée
Schémas Complexes
const ContactFormSchema = z.object({
// Validation string avancée
name: z.string()
.min(1, 'Nom requis')
.min(2, 'Au moins 2 caractères')
.max(50, 'Max 50 caractères')
.regex(/^[a-zA-ZÀ-ÿ\s]+$/, 'Caractères invalides'),
// Email avec domaine whitelist
email: z.string()
.email('Format email invalide')
.refine(
email => ['gmail.com', 'outlook.com', 'company.com'].includes(email.split('@')[1]),
'Domaine email non autorisé'
),
// Numéro de téléphone français
phone: z.string()
.regex(/^(?:(?:\+33|0)[1-9](?:[0-9]{8}))$/, 'Numéro français invalide')
.optional(),
// Âge avec validation métier
age: z.number()
.min(16, 'Vous devez avoir au moins 16 ans')
.max(100, 'Âge invalide'),
// Password avec critères de sécurité
password: z.string()
.min(8, 'Au moins 8 caractères')
.regex(/[A-Z]/, 'Au moins une majuscule')
.regex(/[a-z]/, 'Au moins une minuscule')
.regex(/[0-9]/, 'Au moins un chiffre')
.regex(/[^A-Za-z0-9]/, 'Au moins un caractère spécial'),
// Confirmation mot de passe
confirmPassword: z.string(),
// Acceptation conditions
acceptTerms: z.boolean().refine(val => val === true, 'Conditions requises'),
// Array de hobbies avec validation
hobbies: z.array(
z.object({
name: z.string().min(1),
level: z.enum(['beginner', 'intermediate', 'expert'])
})
).min(1, 'Au moins un hobby').max(5, 'Max 5 hobbies')
})
// Validation cross-field
.refine(data => data.password === data.confirmPassword, {
message: 'Mots de passe différents',
path: ['confirmPassword'] // Erreur sur le champ confirmPassword
})
type ContactFormData = z.infer<typeof ContactFormSchema>
Validation API Routes (Next.js)
Server-Side Validation
// app/api/users/route.ts
import { z } from 'zod'
import { NextRequest, NextResponse } from 'next/server'
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(16).max(100)
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// 🛡️ Validation des données reçues
const userData = CreateUserSchema.parse(body)
// Données validées, on peut les utiliser en sécurité
const user = await createUserInDatabase(userData)
return NextResponse.json({ success: true, user })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Validation failed',
details: error.issues
},
{ status: 400 }
)
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
Techniques Avancées
Schémas Conditionnels
const UserSchema = z.object({
type: z.enum(['admin', 'user']),
name: z.string(),
email: z.string().email()
})
.and(
z.discriminatedUnion('type', [
z.object({
type: z.literal('admin'),
permissions: z.array(z.string()).min(1)
}),
z.object({
type: z.literal('user'),
department: z.string()
})
])
)
Transformation de Données
const DateSchema = z.string()
.datetime()
.transform(str => new Date(str))
const PriceSchema = z.string()
.regex(/^\d+(\.\d{2})?$/)
.transform(str => parseFloat(str))
const ProductSchema = z.object({
name: z.string(),
price: PriceSchema, // string → number
createdAt: DateSchema // string → Date
})