TypeScript en 2025 : Comment la Validation Runtime avec Zod Transforme le Code Type-Safe en Production
Salut HaWkers, vous êtes-vous déjà senti frustré quand TypeScript compile parfaitement mais votre application plante en production à cause de données inattendues ?
Le problème est que TypeScript offre un type-safety UNIQUEMENT au moment de la compilation. Au runtime, JavaScript pur n'a aucune idée de vos types, et les données venant d'APIs, formulaires ou bases de données peuvent casser votre application même avec TypeScript "100% typé".
En 2025, plus de 65% des développeurs utilisent TypeScript, et la communauté a découvert que le type-safety statique n'est pas suffisant. La solution ? Validation runtime avec des bibliothèques comme Zod, qui amènent vos types TypeScript dans le monde réel du JavaScript en exécution.
Le Problème : TypeScript ne Protège pas le Runtime
Voyez ce scénario courant qui casse en production :
// types.ts - Types parfaits !
interface User {
id: number;
name: string;
email: string;
age: number;
}
// api.ts - Semble sûr avec TypeScript
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const user = await response.json() as User; // ⚠️ Mensonge dangereux !
return user;
}
// app.ts - TypeScript est content
const user = await getUser(1);
console.log(user.name.toUpperCase()); // ✅ TypeScript OK
console.log(user.age + 10); // ✅ TypeScript OKMais en production :
// L'API renvoie ceci (champ age en string !) :
{
"id": 1,
"name": "Jean",
"email": "jean@example.com",
"age": "25"
}// Runtime : ERREUR !
console.log(user.age + 10); // "2510" (concaténation de string, pas addition !)
// Pire : ne plante pas, mais comportement incorrectTypeScript ne peut pas prévenir cela parce que as User est juste une "assertion de type" - vous dites au compilateur "fais-moi confiance", mais au runtime, JavaScript ne valide rien.
La Solution : Zod - Validation de Schema + Inférence de Types
Zod est une bibliothèque de validation de schema qui :
- Définit des schemas qui valident les données au runtime
- Infère les types TypeScript automatiquement des schemas
- Garantit le type-safety au compile-time ET runtime
Exemple : Réécriture avec Zod
import { z } from 'zod';
// Le Schema définit validation ET types automatiquement
const UserSchema = z.object({
id: z.number().positive(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).max(150)
});
// Type inféré automatiquement du schema !
type User = z.infer<typeof UserSchema>;
/*
type User = {
id: number;
name: string;
email: string;
age: number;
}
*/
// API avec validation runtime
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Valide au RUNTIME ! Si échoue, lance une erreur
const user = UserSchema.parse(data);
return user; // Garanti type-safe
}
// Utilisation
try {
const user = await getUser(1);
console.log(user.age + 10); // ✅ Garanti que age est un number !
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Données invalides de l\'API:', error.errors);
// [{ path: ['age'], message: 'Expected number, received string' }]
}
}Avantages :
- ✅ Type-safety au compile-time ET runtime
- ✅ Erreurs claires et spécifiques
- ✅ Types inférés automatiquement (DRY - Don't Repeat Yourself)
- ✅ Validation d'APIs, formulaires, variables d'environnement, etc.
Cas d'Usage Avancés avec Zod
1. Validation des Variables d'Environnement
// env.ts - Valide les variables d'environnement au démarrage
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.coerce.number().positive().default(3000), // Convertit string → number
REDIS_HOST: z.string().default('localhost'),
MAX_RETRIES: z.coerce.number().int().min(1).max(10).default(3)
});
export type Env = z.infer<typeof EnvSchema>;
// Valide au démarrage de l'application
export const env = EnvSchema.parse(process.env);
// Maintenant env est totalement type-safe !
console.log(env.PORT + 1); // ✅ TypeScript sait que c'est un number
console.log(env.NODE_ENV); // ✅ Type: 'development' | 'production' | 'test'2. Validation de Formulaires
import { z } from 'zod';
const RegisterFormSchema = z.object({
username: z.string()
.min(3, 'Nom d\'utilisateur doit avoir au moins 3 caractères')
.max(20, 'Nom d\'utilisateur doit avoir au maximum 20 caractères')
.regex(/^[a-zA-Z0-9_]+$/, 'Uniquement lettres, chiffres et underscore'),
email: z.string()
.email('Email invalide')
.toLowerCase(), // Transforme en lowercase automatiquement
password: z.string()
.min(8, 'Mot de passe doit avoir au moins 8 caractères')
.regex(/[A-Z]/, 'Mot de passe doit contenir au moins une majuscule')
.regex(/[0-9]/, 'Mot de passe doit contenir au moins un chiffre'),
confirmPassword: z.string(),
age: z.coerce.number()
.int('L\'âge doit être un nombre entier')
.min(18, 'Vous devez avoir au moins 18 ans')
.max(120, 'Âge invalide'),
termsAccepted: z.literal(true, {
errorMap: () => ({ message: 'Vous devez accepter les conditions' })
})
}).refine(data => data.password === data.confirmPassword, {
message: 'Les mots de passe ne correspondent pas',
path: ['confirmPassword'] // L'erreur apparaît sur ce champ
});
type RegisterForm = z.infer<typeof RegisterFormSchema>;
// Utilisation en React/Vue
function handleSubmit(formData: unknown) {
try {
const validData = RegisterFormSchema.parse(formData);
// validData est type-safe et validé !
await createUser(validData);
} catch (error) {
if (error instanceof z.ZodError) {
// Afficher erreurs par champ
error.errors.forEach(err => {
console.log(`${err.path.join('.')}: ${err.message}`);
});
}
}
}3. Validation de Requêtes API (style tRPC)
import { z } from 'zod';
// Schemas pour les endpoints API
const CreatePostInput = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
tags: z.array(z.string()).min(1).max(5),
published: z.boolean().default(false),
publishedAt: z.date().optional()
});
const UpdatePostInput = CreatePostInput.partial().extend({
id: z.number().positive()
});
// API Routes avec validation automatique
export const api = {
posts: {
create: async (input: unknown) => {
// Valide l'input
const data = CreatePostInput.parse(input);
// data est type-safe !
const post = await db.posts.create({
title: data.title,
content: data.content,
tags: data.tags,
published: data.published
});
return post;
},
update: async (input: unknown) => {
const data = UpdatePostInput.parse(input);
// TypeScript sait que tous les champs sont optionnels sauf id
const post = await db.posts.update({
where: { id: data.id },
data: {
...(data.title && { title: data.title }),
...(data.content && { content: data.content })
}
});
return post;
}
}
};
Zod vs. Alternatives
Comparaison avec Autres Bibliothèques
// Joi - Bibliothèque plus ancienne
import Joi from 'joi';
const joiSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required()
});
// ❌ Problème : Types pas inférés automatiquement
// Vous devez définir l'interface séparément
interface User {
name: string;
age: number;
}
// Yup - Populaire en React
import * as yup from 'yup';
const yupSchema = yup.object({
name: yup.string().required(),
age: yup.number().required()
});
// ⚠️ Types inférés mais moins précis que Zod
type User = yup.InferType<typeof yupSchema>;
// Zod - Meilleure intégration TypeScript
import { z } from 'zod';
const zodSchema = z.object({
name: z.string(),
age: z.number()
});
// ✅ Types inférés parfaitement
type User = z.infer<typeof zodSchema>;Pourquoi Zod domine en 2025 :
- ✅ Zéro dépendances
- ✅ Taille de bundle plus petite (~8kb minifié)
- ✅ Inférence de types supérieure
- ✅ API plus intuitive
- ✅ Meilleure performance
- ✅ Communauté active
Patterns Avancés avec Zod
Transformations et Prétraitement
// Transformer des données pendant la validation
const DateSchema = z.string()
.transform(str => new Date(str))
.refine(date => !isNaN(date.getTime()), 'Date invalide');
const UserWithDateSchema = z.object({
name: z.string(),
birthDate: DateSchema // String → Date automatiquement
});
const user = UserWithDateSchema.parse({
name: 'Jean',
birthDate: '1990-01-15'
});
console.log(user.birthDate); // Objet Date, pas string !Schemas Conditionnels
// Validation conditionnelle basée sur d'autres champs
const PaymentSchema = z.discriminatedUnion('method', [
z.object({
method: z.literal('credit_card'),
cardNumber: z.string().length(16),
cvv: z.string().length(3),
expiryDate: z.string().regex(/^\d{2}\/\d{2}$/)
}),
z.object({
method: z.literal('paypal'),
email: z.string().email()
}),
z.object({
method: z.literal('bank_transfer'),
// Pas de champs supplémentaires nécessaires
})
]);
type Payment = z.infer<typeof PaymentSchema>;
// TypeScript comprend chaque variante !
function processPayment(payment: Payment) {
if (payment.method === 'credit_card') {
// TypeScript sait que cardNumber existe ici
console.log(payment.cardNumber);
} else if (payment.method === 'paypal') {
// TypeScript sait que email existe ici
console.log(payment.email);
}
}Validation Asynchrone
const UniqueUsernameSchema = z.string()
.min(3)
.refine(async (username) => {
// Vérifie dans la BD si username est disponible
const exists = await db.users.findUnique({
where: { username }
});
return !exists;
}, {
message: 'Nom d\'utilisateur déjà utilisé'
});
// Utilisation
const result = await UniqueUsernameSchema.safeParseAsync('john_doe');
if (result.success) {
console.log('Nom d\'utilisateur disponible:', result.data);
} else {
console.log('Erreur:', result.error.errors);
}Intégration avec les Frameworks Modernes
Next.js + tRPC + Zod
// server/routers/posts.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const postsRouter = router({
create: publicProcedure
.input(z.object({
title: z.string().min(1),
content: z.string()
}))
.mutation(async ({ input }) => {
// input est automatiquement type-safe !
return db.posts.create({ data: input });
}),
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.number().optional()
}))
.query(async ({ input }) => {
return db.posts.findMany({
take: input.limit,
...(input.cursor && { cursor: { id: input.cursor } })
});
})
});React Hook Form + Zod
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
email: z.string().email(),
password: z.string().min(8)
});
type FormData = z.infer<typeof FormSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(FormSchema) // Intégration automatique !
});
const onSubmit = (data: FormData) => {
// data est validé et type-safe
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Connexion</button>
</form>
);
}
Performance et Meilleures Pratiques
Réutilisez les Schemas
// ❌ Mauvais : Recrée le schema à chaque appel
function validateUser(data: unknown) {
const schema = z.object({ name: z.string() });
return schema.parse(data);
}
// ✅ Bon : Définit le schema une fois
const UserSchema = z.object({ name: z.string() });
function validateUser(data: unknown) {
return UserSchema.parse(data);
}Utilisez .safeParse() pour le Contrôle d'Erreurs
// ❌ parse() lance une exception
try {
const user = UserSchema.parse(data);
} catch (error) {
// Gestion d'erreur
}
// ✅ safeParse() retourne un résultat
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // Type-safe
} else {
const errors = result.error.errors; // Erreurs structurées
}Si vous voulez maîtriser TypeScript et les patterns modernes de développement, je vous recommande l'article TypeScript en 2025 : Les Top 5 Pratiques pour Maîtriser JavaScript Typé où nous explorons plus de techniques avancées.
C'est parti ! 🦅
💻 Maîtrisez TypeScript Pour de Vrai
TypeScript + Zod représente l'état de l'art en type-safety pour 2025. Mais maîtriser TypeScript nécessite de comprendre JavaScript en profondeur d'abord.
Investissez dans Votre Avenir
J'ai préparé un matériel complet pour vous permettre de maîtriser JavaScript et être prêt pour TypeScript avancé :
Modes de paiement :
- €9,90 (paiement unique)

