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(),
});