Inferindo tipos e convertendo entradas
Um schema que aceita ou lança uma exceção já é suficiente para descrever dados. Para construir algo com ele, faltam mais duas coisas: uma forma de usar essa estrutura no seu próprio código, e uma forma de lidar com uma falha sem precisar de uma exceção.
Comece com um schema descrevendo um professor:
import * as z from 'zod'
const teacherSchema = z.object({
name: z.string(),
age: z.number(),
})Um schema, duas funções
Em TypeScript, normalmente você escreveria essa estrutura uma segunda vez:
type Teacher = {
name: string
age: number
}Agora a mesma estrutura vive em dois lugares, e eles vão divergir assim que alguém adicionar um campo em apenas um deles.
O z.infer extrai o tipo diretamente do schema:
type Teacher = z.infer<typeof teacherSchema>
// { name: string; age: number }Adicione uma chave ao schema e o tipo acompanha sozinho. O schema é a definição, e o TypeScript apenas a lê.
typeof ali parece estranho porque não é o typeof do JavaScript que você já conhece. Essa é a versão do TypeScript, que pergunta "qual é o tipo desse valor" no nível dos tipos. Você pode tratar a linha inteira como uma frase só: me dê o tipo que esse schema descreve.
Falha sem exceção
O parse lança uma exceção. Isso serve bem para um ponto de entrada onde uma requisição ruim deve interromper tudo, mas não serve para nada quando você quer examinar o que deu errado.
O safeParse devolve um resultado em vez disso:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: 21 })
console.log(result)
// { success: true, data: { name: 'Jonathan', age: 21 } }Quando falha, a estrutura muda:
const result = teacherSchema.safeParse({ name: 'Jonathan', age: '21' })
console.log(result.success)
// falseUm sucesso te dá success: true e data. Uma falha te dá success: false e error. Assim, um formulário pode fazer a pergunta e decidir o que fazer:
if (result.success) {
console.log('Seguro para enviar adiante:', result.data)
} else {
console.log('Mostre ao usuário o que deu errado:', result.error.issues)
}O parse lança uma exceção, o que interrompe tudo a menos que você a capture. O safeParse retorna um objeto com uma flag success, então você pode perguntar e continuar.
Restringindo o que você aceita
Um tipo é um filtro grosseiro. z.number() aceita tanto -4 quanto 9e99, e nenhuma das duas é a idade de um professor.
Métodos são encadeados ao schema para deixá-lo mais restrito:
const teacherSchema = z.object({
name: z.string(),
age: z.number().min(18),
isAmerican: z.boolean().optional(),
id: z.number().default(() => Math.random()),
}).min(18)rejeita qualquer valor abaixo de dezoito..gte()e.lte()fazem o mesmo trabalho com comparações explícitas..optional()permite que a chave esteja totalmente ausente..default()fornece um valor quando a chave está ausente, então o objeto validado sempre tem um.
O Zod também já vem com verificações prontas para formatos comuns, então um e-mail é uma única chamada:
const contactSchema = z.object({
email: z.email(),
})
contactSchema.parse({ email: '[email protected]' }) // ok
contactSchema.parse({ email: 'jabbahuttcorp.com' }) // ZodErrorÉ isso que a biblioteca quer dizer com declarativo: você descreve o resultado que quer, e ela cuida da verificação.
Convertendo o que chega
Campos de formulário HTML enviam strings. Todos eles, inclusive o campo rotulado "idade" com um seletor numérico ao lado.
Então um schema que espera z.number() rejeita uma entrada de formulário perfeitamente válida, porque '13' é uma string. A conversão transforma o valor primeiro, e só depois valida:
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
})
characterSchema.parse({ name: 'Luke Skywalker', episode: '4' })
// { name: 'Luke Skywalker', episode: 4 }A string entrou, um número saiu, e qualquer outra verificação nessa chave rodou sobre o número.
Então z.coerce.number().min(18) converte o texto em número e só então pergunta se esse número é pelo menos dezoito.
Coloque a mão na massa
Partindo deste schema e deste objeto:
const characterSchema = z.object({
name: z.string(),
episode: z.number(),
})
const character = {
name: 'Jabba the Hutt',
episode: '6',
}Faça quatro alterações:
- Crie um tipo
Characterinferido do schema, e anote o objeto com ele. - Adicione uma chave booleana opcional
isJediao schema. - Faça
episodeaceitar a string'6'e armazená-la como número. - Registre no console se a validação passou ou falhou, como um único booleano.
Compare suas respostas
type Character = z.infer<typeof characterSchema>
const characterSchema = z.object({
name: z.string(),
episode: z.coerce.number(),
isJedi: z.boolean().optional(),
})
const character: Character = {
name: 'Jabba the Hutt',
episode: '6',
}
console.log(characterSchema.safeParse(character).success)
// trueQuatro observações sobre as quatro alterações:
z.infer<typeof characterSchema>extrai o tipo diretamente do schema, então adicionarisJedijá atualiza o tipo, sem precisar editar nada de novo..optional()significa que a chave pode estar ausente, e é por isso quecharacternão precisa terisJedie ainda assim passa na validação.z.coerce.number()converte antes de verificar, então a string'6'vira o número6..successé o booleano. Registrar apenassafeParse(...)no console imprime o objeto de resultado inteiro, eparsete daria os dados ou uma exceção, e nenhum dos dois é um booleano.
Um detalhe que vale notar: com episode sendo convertido, o tipo anotado diz number, enquanto o objeto literal contém '6'. Essa é a diferença entre as estruturas de entrada e saída, e é por isso que ler result.data em vez do objeto original importa tanto.
Para onde isso vai a seguir
Um safeParse que falha devolve um error, e até aqui isso foi apenas algo cuja existência a gente verificava, não algo que a gente de fato lia.
Lendo erros de validação abre esse assunto: qual campo falhou, o que havia de errado com ele, e como transformar isso em mensagens que uma pessoa consiga usar para agir.

