Site icon Blog ARC Optimizer

Zod + TypeScript : la validation de schéma simplifiée

Zod + TypeScript : la validation de schéma simplifiée


Découvrez comment utiliser Zod, une bibliothèque de validation de schéma TypeScript qui vous permet de valider des données externes au moment de l’exécution.

Manuscrit est rapidement devenu l’un des langages les plus populaires pour le développement Web. En tant que sur-ensemble de JavaScript qui ajoute un typage statique facultatif et d’autres fonctionnalités, TypeScript offre des avantages aux équipes de développement cherchant à écrire un code plus robuste et plus maintenable.

Il y a un domaine dans lequel TypeScript seul ne suffit pas : la validation d’exécution. Lorsque les données proviennent de sources externes (réponses API, soumissions de formulaires ou entrées utilisateur), TypeScript ne peut pas garantir que les données correspondent à nos types attendus au moment de l’exécution. C’est là qu’une bibliothèque comme Zod entre.

Zod est une bibliothèque de validation de schéma basée sur TypeScript qui comble le fossé entre la sécurité des types au moment de la compilation et la validation à l’exécution. Il nous permet de définir des schémas qui non seulement valident les données au moment de l’exécution, mais qui déduisent également automatiquement les types TypeScript, nous offrant ainsi le meilleur des deux mondes. Dans cet article, nous explorerons comment Zod facilite la validation des schémas et comment il s’intègre parfaitement aux applications TypeScript et React.

Qu’est-ce que Zod et pourquoi en avons-nous besoin ?

Avant de plonger dans Zod, comprenons le problème qu’il résout. Prenons un scénario typique dans lequel nous récupérons les données utilisateur à partir d’une API :

interface User {
  name: string;
  email: string;
  age: number;
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();

  
  return data as User;
}

L’assertion de type as User dit à TypeScript de s’assurer que les données correspondent à notre interface utilisateur, mais il ne s’agit que d’une hypothèse au moment de la compilation. Si l’API renvoie des données avec une structure différente ou des champs manquants, notre application pourrait planter au moment de l’exécution.

Zod résout ce problème en nous permettant de définir des schémas qui valident les données au moment de l’exécution tout en déduisant automatiquement les types TypeScript :

import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().positive()
});


type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  
  return UserSchema.parse(data); 
}

Dans l’exemple de code ci-dessus, nous définissons un schéma Zod en utilisant z.object() qui décrit la structure attendue et les règles de validation de nos données Utilisateur. Le z.infer<typeof UserSchema> génère automatiquement un type TypeScript à partir de notre schéma, et UserSchema.parse() valide les données entrantes au moment de l’exécution.

Nous disposons désormais d’une validation d’exécution qui vérifie que nos données correspondent à la structure attendue, et TypeScript connaît automatiquement le type. Aucune définition manuelle de l’interface n’est nécessaire !

Premiers pas avec Zod

Pour démarrer avec Zod, nous devons d’abord l’installer :

npm install zod

Zod a besoin mode strict activé dans notre tsconfig.json:

{
  "compilerOptions": {
    "strict": true
  }
}

Une fois installé, nous pouvons commencer à définir des schémas à l’aide de l’API intuitive de Zod.

Types de schémas de base

Zod fournit des schémas pour tous les types JavaScript primitifs et de nombreux modèles courants. Explorons les éléments de base fondamentaux :

Primitives

import { z } from 'zod';


const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();


stringSchema.parse("hello"); 
stringSchema.parse(42); 


const result = numberSchema.safeParse("not a number");
if (!result.success) {
  console.log(result.error); 
} else {
  console.log(result.data); 
}

Ici, nous créons des schémas individuels pour chaque type primitif à l’aide des validateurs de base de Zod. Le parse() La méthode valide et renvoie les données si elles correspondent au type attendu, tandis que safeParse() renvoie un objet de résultat qui ne génère pas d’erreurs, ce qui le rend plus sûr pour la validation des entrées utilisateur.

Validations de chaînes

Zod brille en matière de validations de chaînes. Il fournit des validateurs intégrés pour les formats courants :

const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();


const usernameSchema = z.string()
  .min(3, "Username must be at least 3 characters")
  .max(20, "Username must be at most 20 characters")
  .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores");


const normalizedEmail = z.string()
  .email()
  .toLowerCase()
  .trim();

normalizedEmail.parse("  USER@EXAMPLE.COM  ");

Cet exemple démontre les puissantes capacités de validation de chaînes de Zod. Nous pouvons enchaîner plusieurs validateurs, tels que .email(), .min(), .max() et .regex() pour créer des règles de validation complètes. Les méthodes de transformation comme .toLowerCase() et .trim() normaliser automatiquement les données pendant l’analyse.

Nombres et contraintes numériques

const ageSchema = z.number()
  .int("Age must be a whole number")
  .positive("Age must be positive")
  .max(120, "Please enter a valid age");

const priceSchema = z.number()
  .multipleOf(0.01) 
  .nonnegative();

Pour les nombres, Zod fournit des validateurs pour les contraintes typiques telles que .int() pour les entiers, .positive() pour les nombres positifs et .multipleOf() pour des incréments spécifiques. Ceux-ci sont particulièrement utiles pour la validation de formulaires où vous devez vérifier que la saisie numérique répond aux exigences de l’entreprise.

Types de schémas complexes

La véritable puissance de Zod réside dans la composition de schémas simples en structures complexes qui correspondent aux modèles de données d’une application.

Objets

Les objets sont probablement le type complexe le plus courant avec lequel nous travaillerons :

const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  state: z.string().length(2),
  zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
});

const PersonSchema = z.object({
  firstName: z.string(),
  lastName: z.string(),
  email: z.string().email(),
  age: z.number().int().positive(),
  address: AddressSchema, 
  phoneNumbers: z.array(z.string()), 
  isActive: z.boolean().default(true) 
});

type Person = z.infer<typeof PersonSchema>;

Cet exemple montre comment Zod gère la validation des objets imbriqués. Le PersonSchema contient le AddressSchema en tant que propriété imbriquée, démontrant comment des structures de données complexes peuvent être validées de manière récursive. Zod gère automatiquement la validation du tableau avec z.array() et fournit des valeurs par défaut avec .default().

Nous pouvons également modifier les schémas d’objets à l’aide de méthodes utiles :


const PartialPersonSchema = PersonSchema.partial();


const PersonNameSchema = PersonSchema.pick({ firstName: true, lastName: true });


const PersonWithoutAddressSchema = PersonSchema.omit({ address: true });


const EmployeeSchema = PersonSchema.extend({
  employeeId: z.string(),
  department: z.string()
});

Tableaux et tuples


const tagsSchema = z.array(z.string()).min(1, "At least one tag is required");
const scoresSchema = z.array(z.number()).length(5, "Exactly 5 scores required");


const coordinateSchema = z.tuple([z.number(), z.number()]); 
const rgbSchema = z.tuple([
  z.number().int().min(0).max(255),
  z.number().int().min(0).max(255),
  z.number().int().min(0).max(255)
]); 

Les tableaux dans Zod peuvent avoir des contraintes de longueur en utilisant .min(), .max() et .length(). Les tuples sont parfaits pour les tableaux de longueur fixe où chaque position a un type spécifique, comme des coordonnées ou des valeurs de couleur RVB. Le schéma tuple restreint à la fois la longueur et le type de chaque élément.

Syndicats et syndicats discriminés

Les unions nous permettent de valider des données qui peuvent être de plusieurs types :


const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse("hello"); 
stringOrNumber.parse(42); 
stringOrNumber.parse(true); 


const ResponseSchema = z.discriminatedUnion("status", [
  z.object({
    status: z.literal("success"),
    data: z.string()
  }),
  z.object({
    status: z.literal("error"),
    error: z.string(),
    code: z.number()
  })
]);




type Response = z.infer<typeof ResponseSchema>;

Les unions permettent aux données d’être de plusieurs types, tandis que les unions discriminées sont optimisées pour les objets partageant une propriété commune (le discriminateur). Le z.discriminatedUnion() crée une validation plus efficace en vérifiant d’abord le champ discriminateur, puis en appliquant le schéma approprié. Ceci est idéal pour gérer différents types de réponses ou variations d’état.

Validation de formulaire avec Zod et KendoReact

L’un des cas d’utilisation les plus courants pour la validation d’exécution est gestion des formulaires. Créons un formulaire d’inscription étape par étape pour voir comment Zod s’intègre à un Composant React Form de la Bibliothèque d’interface utilisateur Progress KendoReact.

Étape 1 : configuration des composants de base du formulaire

Tout d’abord, créons la structure de base du formulaire à l’aide des composants KendoReact Form :

import * as React from 'react';
import {
  Form,
  Field,
  FormElement,
  FieldRenderProps,
  FormRenderProps,
} from '@progress/kendo-react-form';
import { Input } from '@progress/kendo-react-inputs';
import { Error } from '@progress/kendo-react-labels';
import { Button } from '@progress/kendo-react-buttons';


const FormInput = (fieldRenderProps: FieldRenderProps) => {
  const { validationMessage, visited, ...others } = fieldRenderProps;
  return (
    <div className="k-form-field-wrap">
      <Input {...others} labelClassName={'k-form-label'} />
      {visited && validationMessage && (
        <Error>
          <span>{validationMessage}</span>
        </Error>
      )}
    </div>
  );
};

const App = () => {
  const handleSubmit = (dataItem: { [name: string]: any }) => {
    console.log('Form submitted:', dataItem);
  };

  return (
    <Form
      onSubmit={handleSubmit}
      render={(formRenderProps: FormRenderProps) => (
        <FormElement style={{ maxWidth: 400 }}>
          <fieldset className={'k-form-fieldset'}>
            <legend className={'k-form-legend'}>Create Your Account</legend>

            <Field
              name={'username'}
              component={FormInput}
              label={'Username'}
            />

            <Field
              name={'email'}
              component={FormInput}
              label={'Email'}
              type={'email'}
            />

            <Field
              name={'password'}
              component={FormInput}
              label={'Password'}
              type={'password'}
            />

            <Field
              name={'age'}
              component={FormInput}
              label={'Age'}
              type={'number'}
            />

            <div className="k-form-buttons">
              <Button
                themeColor={'primary'}
                type={'submit'}
                disabled={!formRenderProps.allowSubmit}
              >
                Register
              </Button>
              <Button onClick={formRenderProps.onFormReset}>Clear</Button>
            </div>
          </fieldset>
        </FormElement>
      )}
    />
  );
};

export default App;

Dans cette configuration de base, nous créons un FormInput composant qui enveloppe le Entrée KendoReact avec une logique d’affichage des erreurs, et créez un formulaire avec quatre champs (nom d’utilisateur, email, mot de passe, âge) à l’aide du formulaire KendoReact, Champ et Élément de formulaire composants. La structure du formulaire est prête, mais il lui manque une validation. Cela crée notre structure de formulaire de base, y compris les entrées stylisées et la gestion des erreurs.

Étape 2 : ajout du schéma Zod et validation

Ajoutons maintenant Zod pour fournir une validation de type sécurisé. Nous allons importer Zod et créer notre schéma de validation :

import { z } from 'zod';


const RegistrationSchema = z.object({
  username: z
    .string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be at most 20 characters'),
  email: z.string().email({ message: 'Please enter a valid email address' }),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .regex(/[0-9]/, 'Password must contain at least one number'),
  age: z
    .string()
    .transform((val) => parseInt(val, 10))
    .pipe(
      z.number()
        .int('Age must be a whole number')
        .min(18, 'You must be at least 18 years old')
        .max(120, 'Please enter a valid age')
    ),
});


type RegistrationData = z.infer<typeof RegistrationSchema>;


const createZodValidator = (schema: z.ZodSchema) => {
  return (value: any) => {
    const result = schema.safeParse(value);
    if (!result.success) {
      return result.error.issues[0]?.message || 'Invalid value';
    }
    return '';
  };
};

Nous définissons ici les règles de validation pour chaque champ :

  • Contraintes de longueur du nom d’utilisateur (3 à 20 caractères)
  • Validation du format d’e-mail
  • Exigences du mot de passe (plus de 8 caractères avec majuscules et chiffres)
  • Restrictions d’âge (18-120) avec transformation chaîne en nombre puisque les entrées HTML renvoient des chaînes.

Le createZodValidator La fonction d’assistance convertit les schémas Zod en validateurs compatibles KendoReact, et nous déduisons également automatiquement un type TypeScript à partir de notre schéma.

Étape 3 : Intégration de la soumission du formulaire avec validation complète

Enfin, nous mettrons à jour notre formulaire pour utiliser les validateurs et gérer la soumission du formulaire avec une validation Zod complète. Cela fait que le code complet de notre composant ressemble à ce qui suit :

import * as React from 'react';
import {
  Form,
  Field,
  FormElement,
  FieldRenderProps,
  FormRenderProps,
} from '@progress/kendo-react-form';
import { Input } from '@progress/kendo-react-inputs';
import { Error } from '@progress/kendo-react-labels';
import { Button } from '@progress/kendo-react-buttons';
import { z } from 'zod';


const RegistrationSchema = z.object({
  username: z
    .string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be at most 20 characters'),
  email: z.email({ message: 'Please enter a valid email address' }),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .regex(/[0-9]/, 'Password must contain at least one number'),
  age: z
    .string()
    .transform((val) => parseInt(val, 10))
    .pipe(
      z
        .number()
        .int('Age must be a whole number')
        .min(18, 'You must be at least 18 years old')
        .max(120, 'Please enter a valid age')
    ),
});


type RegistrationData = z.infer<typeof RegistrationSchema>;


const createZodValidator = (schema: z.ZodSchema) => {
  return (value: any) => {
    const result = schema.safeParse(value);
    if (!result.success) {
      return result.error.issues[0]?.message || 'Invalid value';
    }
    return '';
  };
};


const FormInput = (fieldRenderProps: FieldRenderProps) => {
  const { validationMessage, visited, ...others } = fieldRenderProps;
  return (
    <div className="k-form-field-wrap">
      <Input {...others} labelClassName={'k-form-label'} />
      {visited && validationMessage && (
        <Error>
          <span>{validationMessage}</span>
        </Error>
      )}
    </div>
  );
};

const App = () => {
  const handleSubmit = (dataItem: { [name: string]: any }) => {
    
    const result = RegistrationSchema.safeParse(dataItem);

    if (result.success) {
      
      const data: RegistrationData = result.data;
      console.log('Registration successful:', data);
      
    } else {
      
      console.error('Validation failed:', result.error);
    }
  };

  return (
    <Form
      onSubmit={handleSubmit}
      render={(formRenderProps: FormRenderProps) => (
        <FormElement style={{ maxWidth: 400 }}>
          <fieldset className={'k-form-fieldset'}>
            <legend className={'k-form-legend'}>Create Your Account</legend>

            <Field
              name={'username'}
              component={FormInput}
              label={'Username'}
              validator={createZodValidator(RegistrationSchema.shape.username)}
            />

            <Field
              name={'email'}
              component={FormInput}
              label={'Email'}
              type={'email'}
              validator={createZodValidator(RegistrationSchema.shape.email)}
            />

            <Field
              name={'password'}
              component={FormInput}
              label={'Password'}
              type={'password'}
              validator={createZodValidator(RegistrationSchema.shape.password)}
            />

            <Field
              name={'age'}
              component={FormInput}
              label={'Age'}
              type={'number'}
              validator={createZodValidator(RegistrationSchema.shape.age)}
            />

            <div className="k-form-buttons">
              <Button
                themeColor={'primary'}
                type={'submit'}
                disabled={!formRenderProps.allowSubmit}
              >
                Register
              </Button>
              <Button onClick={formRenderProps.onFormReset}>Clear</Button>
            </div>
          </fieldset>
        </FormElement>
      )}
    />
  );
};

export default App;

Dans cette dernière étape, nous connectons chaque champ à son validateur Zod respectif en utilisant RegistrationSchema.shape.fieldNamece qui nous donne accès aux schémas de champs individuels. Le handleSubmit La fonction effectue une validation finale de l’ensemble des données du formulaire, assurant la sécurité du type et une validation complète avant de traiter la soumission. Désormais, nous remarquerons que des erreurs de validation au niveau du champ apparaissent en temps réel lorsque les utilisateurs interagissent avec le formulaire.

Cette intégration nous offre le meilleur des deux mondes : de puissants composants de formulaire KendoReact avec la validation robuste de Zod et l’inférence automatique de type TypeScript !

Si vous souhaitez interagir avec cet exemple de formulaire et les éléments associés, assurez-vous de consulter ce qui suit Projet StackBlitz.

Conclure

Zod rassemble le meilleur de la sécurité des types au moment de la compilation et de la validation de l’exécution de TypeScript d’une manière naturelle et intuitive. En définissant nos schémas une seule fois, nous obtenons à la fois la validation et l’inférence de type, éliminant ainsi la duplication et les inadéquations potentielles entre nos types TypeScript et la logique de validation.

À mesure que nos applications gagnent en complexité et gèrent des sources de données plus diverses, disposer d’une couche de validation fiable devient de plus en plus essentiel. Zod fournit non seulement cette fiabilité, mais il le fait d’une manière qui améliore notre expérience de développement grâce à une excellente intégration TypeScript et des messages d’erreur clairs et personnalisables. Cela facilite vraiment la validation des schémas !




Source link
Quitter la version mobile