00
:
00
:
00
:
00
Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

Schemi Ricorsivi e Lazy

z.lazy()

z.lazy() permette di definire schemi che fanno riferimento a se stessi. E’ essenziale per strutture dati ricorsive come alberi, liste concatenate e strutture annidate.

import { z } from "zod";

// Definiamo un tipo per una categoria con sotto-categorie
type Categoria = {
  nome: string;
  sottoCategorie: Categoria[];
};

const CategoriaSchema: z.ZodType<Categoria> = z.object({
  nome: z.string(),
  sottoCategorie: z.lazy(() => CategoriaSchema.array()),
});

CategoriaSchema.parse({
  nome: "Elettronica",
  sottoCategorie: [
    {
      nome: "Smartphone",
      sottoCategorie: [
        { nome: "Android", sottoCategorie: [] },
        { nome: "iOS", sottoCategorie: [] },
      ],
    },
    { nome: "Laptop", sottoCategorie: [] },
  ],
}); // OK

Nota importante: Con z.lazy() devi dichiarare esplicitamente il tipo TypeScript (z.ZodType<Categoria>) perche’ Zod non riesce a inferirlo automaticamente per schemi ricorsivi.

Alberi (Tree)

Un classico albero binario:

type AlberoBinario = {
  valore: number;
  sinistro: AlberoBinario | null;
  destro: AlberoBinario | null;
};

const AlberoSchema: z.ZodType<AlberoBinario> = z.object({
  valore: z.number(),
  sinistro: z.lazy(() => AlberoSchema).nullable(),
  destro: z.lazy(() => AlberoSchema).nullable(),
});

AlberoSchema.parse({
  valore: 10,
  sinistro: {
    valore: 5,
    sinistro: { valore: 3, sinistro: null, destro: null },
    destro: { valore: 7, sinistro: null, destro: null },
  },
  destro: {
    valore: 15,
    sinistro: null,
    destro: { valore: 20, sinistro: null, destro: null },
  },
}); // OK

Linked List

Una lista concatenata:

type NodoLista = {
  valore: string;
  prossimo: NodoLista | null;
};

const NodoSchema: z.ZodType<NodoLista> = z.object({
  valore: z.string(),
  prossimo: z.lazy(() => NodoSchema).nullable(),
});

NodoSchema.parse({
  valore: "primo",
  prossimo: {
    valore: "secondo",
    prossimo: {
      valore: "terzo",
      prossimo: null,
    },
  },
}); // OK

JSON Type

Definire uno schema per un valore JSON generico:

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [chiave: string]: JsonValue };

const JsonSchema: z.ZodType<JsonValue> = z.lazy(() =>
  z.union([
    z.string(),
    z.number(),
    z.boolean(),
    z.null(),
    z.array(JsonSchema),
    z.record(JsonSchema),
  ])
);

// Valida qualsiasi JSON valido
JsonSchema.parse("stringa");                    // OK
JsonSchema.parse(42);                            // OK
JsonSchema.parse(null);                          // OK
JsonSchema.parse([1, "due", [true, null]]);      // OK
JsonSchema.parse({ a: 1, b: { c: [1, 2] } });   // OK

Commenti Annidati

Un esempio pratico di struttura ricorsiva: commenti con risposte:

type Commento = {
  autore: string;
  testo: string;
  dataCreazione: string;
  risposte: Commento[];
};

const CommentoSchema: z.ZodType<Commento> = z.object({
  autore: z.string().min(1),
  testo: z.string().min(1),
  dataCreazione: z.string().datetime(),
  risposte: z.lazy(() => z.array(CommentoSchema)),
});

Branded Types

I branded types aggiungono un “marchio” al tipo inferito, impedendo di confondere tipi strutturalmente identici.

const UserId = z.string().uuid().brand<"UserId">();
const PostId = z.string().uuid().brand<"PostId">();

type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;

// Le funzioni accettano solo il tipo branded corretto
function getUtente(id: UserId) {
  // ...
}

function getPost(id: PostId) {
  // ...
}

const userId = UserId.parse("550e8400-e29b-41d4-a716-446655440000");
const postId = PostId.parse("660e8400-e29b-41d4-a716-446655440000");

getUtente(userId); // OK
getUtente(postId); // Errore TypeScript! PostId non e' assegnabile a UserId

Branded Types Pratici

const Email = z.string().email().brand<"Email">();
const Prezzo = z.number().positive().brand<"Prezzo">();
const Cap = z.string().length(5).regex(/^\d+$/).brand<"Cap">();

type Email = z.infer<typeof Email>;
type Prezzo = z.infer<typeof Prezzo>;

function inviaEmail(destinatario: Email, oggetto: string) {
  // Sicuro: `destinatario` e' sicuramente una email validata
}

const email = Email.parse("utente@esempio.it");
inviaEmail(email, "Benvenuto!"); // OK
inviaEmail("stringa-generica" as any, "Test"); // Errore a runtime se non validata

z.custom()

Crea uno schema completamente personalizzato con una funzione di validazione:

// Validare che un valore sia un'istanza di una classe
class MioOggetto {
  constructor(public nome: string) {}
}

const MioOggettoSchema = z.custom<MioOggetto>(
  (val) => val instanceof MioOggetto,
  { message: "Deve essere un'istanza di MioOggetto" }
);

MioOggettoSchema.parse(new MioOggetto("test")); // OK
MioOggettoSchema.parse({ nome: "test" });         // Errore

z.custom() per Tipi Complessi

// Validare un codice fiscale italiano
const CodiceFiscale = z.custom<string>(
  (val) => {
    if (typeof val !== "string") return false;
    return /^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/.test(val);
  },
  { message: "Codice fiscale non valido" }
);

// Validare una partita IVA
const PartitaIva = z.custom<string>(
  (val) => {
    if (typeof val !== "string") return false;
    return /^\d{11}$/.test(val);
  },
  { message: "Partita IVA non valida" }
);

z.instanceof()

Valida che il valore sia un’istanza di una classe:

const FileSchema = z.instanceof(File);
const BufferSchema = z.instanceof(Buffer);
const ErroreSchema = z.instanceof(Error);

// Utile per validare upload di file
const UploadSchema = z.object({
  file: z.instanceof(File),
  nome: z.string(),
});