Config type-safe em Node.js com TypeScript: Zod, generics e utility types
Por John Pires · 03 de abril de 2026

Config type-safe em Node.js com TypeScript: Zod, generics e utility types
O que é e por que importa
Definição direta
Config type-safe é a prática de ler valores de configuração (variáveis de ambiente, arquivos .env, JSON, etc.) e transformá-los em um objeto tipado de maneira que o TypeScript saiba, em tempo de compilação, quais campos existem, seus tipos e possivelmente valores padrão. Em runtime, validações garantem que os valores recebidos seguem as regras esperadas.
Por que isso importa no dia a dia
Sem validação e tipagem forte, bugs simples acontecem: uma porta lida como string, uma URL faltando, credenciais nulas causando erro em produção. Com validação e tipagem você reduz surpresas, melhora a experiência do desenvolvedor e evita erros caros em produção. A segurança aqui é sobre confiabilidade do sistema, não apenas sobre segurança de dados.
Usar bibliotecas como Zod permite declarar uma fonte única de verdade (schema) que serve tanto para validação em runtime quanto para inferir tipos estáticos no TypeScript. Generics e utility types ajudam a escrever funções reutilizáveis e evitar duplicação entre validação e tipo.
Conceitos fundamentais com analogias
Config como contrato
Pense em configuração como um contrato entre a sua aplicação e o ambiente. O contrato lista chaves, tipos e regras. Quando você usa Zod, você escreve esse contrato em código: ele valida o que chega e o TypeScript pode ler esse contrato para inferir tipos.
Tipos em compile-time vs validação em runtime
TypeScript protege você enquanto escreve código (compile-time). Mas durante execução, apenas JavaScript roda — e valores externos (process.env) chegam como strings. Zod atua em runtime, convertendo/checando esses valores. A analogia: TypeScript é a planta do prédio; Zod inspeciona se o que foi construído segue a planta.
Generics permitem criar funções que trabalham com qualquer schema Zod sem perder tipos, como um molde reutilizável que aceita diferentes plantas e retorna objetos corretos conforme cada planta.
Passo a passo com codigo comentado
Preparação do projeto
Comece com um projeto Node + TypeScript. Instale dependências essenciais: zod e dotenv. Não é obrigatório, mas dotenv facilita carregar variáveis locais durante desenvolvimento.
Exemplos de comandos (não é código TypeScript):
- npm init -y
- npm install zod dotenv
- npm install -D typescript ts-node @types/node
Schema Zod básico e inferência de tipo
Agora criaremos o schema. A ideia: declarar como cada variável deve ser tratada e usar z.infer para extrair o tipo automaticamente.
Contexto: este arquivo define o schema de configuração e extrai o tipo TypeScript correspondente.
import { z } from 'zod'
// Definimos o schema com zod
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.preprocess((v) => {
// convertendo string para number quando necessário
if (typeof v === 'string' && v.trim() !== '') return Number(v)
return v
}, z.number().int().positive().default(3000)),
DATABASE_URL: z.string().min(1),
ENABLE_FEATURE_X: z.preprocess((v) => {
if (typeof v === 'string') return v === 'true'
return Boolean(v)
}, z.boolean().default(false))
})
// Inferimos o tipo TypeScript automaticamente a partir do schema
export type Env = z.infer<typeof EnvSchema>
export { EnvSchema }
O que acontece: o schema transforma strings em numbers/booleans conforme necessário, aplica defaults e exige campos obrigatórios. O tipo Env é derivado do schema, garantindo que o TypeScript opere com o formato validado.
Loader genérico que retorna tipos derivados do schema
Contexto: queremos uma função reutilizável que receba qualquer schema Zod e retorne o objeto validado e tipado. Usaremos generics para que the return type seja z.infer do schema recebido.
import { ZodType, ZodTypeAny } from 'zod'
import { EnvSchema } from './schema'
// Função genérica: aceita qualquer schema Zod e retorna o tipo inferido desse schema
export function loadConfig<T extends ZodTypeAny>(schema: T, env: Record<string, unknown> = process.env) {
const result = schema.safeParse(env)
if (!result.success) {
// Em aplicações reais você pode logar erros formatados e falhar o startup
const formatted = result.error.format()
throw new Error('Config validation error: ' + JSON.stringify(formatted))
}
return result.data as z.infer<T>
}
// Uso específico
export const config = loadConfig(EnvSchema)
O que acontece: loadConfig valida env usando o schema passado. Se falhar, lança erro com detalhes; se passar, retorna um objeto tipado. O cast final ajuda o TypeScript a entender o retorno como z.infer do schema.
Exemplo pratico completo
Contexto do exemplo
Vamos montar uma aplicação mínima que expõe uma rota health e inicia um servidor Express usando as configurações tipadas. O objetivo é mostrar código real que roda e consome o config validado.
Implementação completa (server.ts)
Contexto: arquivo que inicializa dotenv, carrega config e inicia Express. Inclui tratamento de erro de validação.
import 'dotenv/config' // carrega variáveis do .env para process.env
import express from 'express'
import { config } from './config' // import do loader genérico que retorna o objeto tipado
const app = express()
app.get('/health', (req, res) => {
res.json({ ok: true, env: config.NODE_ENV })
})
const port = config.PORT
app.listen(port, () => {
console.log('Server running on port', port)
})
O que acontece: ao importar config, o loader já valida process.env conforme EnvSchema. Quando o servidor sobe, o TypeScript garante que config.PORT é number e config.DATABASE_URL é string, evitando conversões manuais espalhadas.
Um exemplo de teste rápido
Contexto: às vezes você quer testar o loader em isolamento com valores falsos para simular diferentes ambientes. Isso é simples com nossa função genérica que aceita um 'env' override.
import { loadConfig } from './loader'
import { EnvSchema } from './schema'
const fakeEnv = {
NODE_ENV: 'test',
PORT: '4000',
DATABASE_URL: 'postgres://local',
ENABLE_FEATURE_X: 'true'
}
const cfg = loadConfig(EnvSchema, fakeEnv)
console.log(typeof cfg.PORT, cfg.PORT, cfg.ENABLE_FEATURE_X)
// Saída esperada: 'number' 4000 true
O que acontece: o loader converte PORT para number e ENABLE_FEATURE_X para boolean conforme definido no schema. Isso permite escrever testes onde você controla o ambiente e verifica comportamento sem mexer em process.env globalmente.
Erros comuns, armadilhas e dicas práticas
Erros comuns
Listo os erros mais frequentes que vejo em projetos iniciais:
- Confiar que process.env já tem tipos corretos — todas as variáveis chegam como string ou undefined.
- Validar no começo mas continuar usando process.env diretamente no resto do código.
- Fazer parse no momento do import do módulo que define config pode dificultar testes isolados e reinicialização de módulos.
- Usar any para contornar tipos em vez de ajustar o schema e tipos corretamente.
Dicas práticas
Preferir safeParse em desenvolvimento para obter erros formatados. Use mensagens claras para o time dev e logs legíveis em produção.
Se o seu config falhar na inicialização, é melhor a aplicação não subir com um estado parcial — falhe rápido e com mensagem clara.
Outra armadilha: defaults no schema devem refletir valores sensíveis (ex: não coloque secrets padrão). Use defaults apenas para conveniências como porta ou flags de feature.
Proximos passos para aprofundar
Integração com CI e checks
Valide o schema em pipeline com conjuntos de variáveis por ambiente. Você pode rodar scripts que carregam schemas e validam arquivos .env.example ou arquivos de secret management. Isso evita mudanças em produção que quebrem o startup.
Outras bibliotecas e trade-offs
Zod é ótimo para DX e integração com TypeScript. Alternativas: io-ts (mais funcional), yup (menor integração com tipos inferidos automáticos). Avalie se quer runtime menor ou APIs diferentes. Zod costuma ser a escolha pragmática em projetos TypeScript atuais.
Estudar utility types do TypeScript ajuda a criar variantes do tipo de config: por exemplo, criar uma versão parcial para testes ou uma versão só com chaves sensíveis para logs.
Conclusão prática
Resumo das ações recomendadas
Para aplicar config type-safe em um projeto Node + TypeScript, siga estes passos práticos: 1) declare schemas Zod para todo config; 2) crie um loader genérico que valida e retorna tipos inferidos; 3) passe a usar apenas o objeto validado no restante da aplicação; 4) rode validações no CI para garantir que variantes de ambiente permanecem consistentes.
Fechamento e próximos comandos
Pratique convertendo um arquivo .env do projeto para um schema Zod e substitua usos diretos de process.env por import do config validado. Em seguida, adicione um job no CI que executa um script de validação para .env.example e falhe o build se algo estiver faltando.
Seguir esses passos reduz bugs de tipo, melhora a previsibilidade do sistema e ainda mantém o TypeScript e Zod como fonte única de verdade para configuração.
Se quiser, posso gerar um template de projeto com todos os arquivos mostrados (schema, loader, server, scripts de CI) pronto para clonar e rodar: diga o nome do gerenciador de pacotes e eu monto os scripts.
Assuntos: TypeScript, Node.js, zod, generics, config
Leia também
Config type-safe em TypeScript: loader genérico com merges e type guards
Padrão prático para loaders de config em TypeScript: substitua casts por um loader genérico que aplica defaults, faz deep merge e valida com type guards…
Configuração tipada em TypeScript: env, generics e decorators práticos
Como evitar casts perigosos em process.env e construir loaders tipados em TypeScript usando generics e decorators. Padrões práticos, erros comuns e…
Converter JSON snake_case para camelCase com TypeScript e Generics
Aprenda passo a passo a converter JSON snake_case para camelCase usando TypeScript: funções runtime recursivas, tipos genéricos DeepCamelCase e boas práticas…
Autorização type-safe em Node/Express com decorators, branded types e generics
Padrões práticos para autorização type-safe em Node/Express usando decorators, branded types e generics: evite strings mágicas, typagem de IDs e centralize…
EventEmitter type-safe em TypeScript com generics e mapped types
Aprenda a construir um EventEmitter type-safe em TypeScript usando generics e mapped types: do problema de any ao emitter tipado, com exemplos práticos…
Feature flags type-safe em TypeScript: configuração, generics e type guards
Aprenda a implementar feature flags type-safe em TypeScript: defina keys literais, use generics, type guards e decorators para evitar typos e validar…