DTOs type-safe em TypeScript: decorators, generics e type guards passo a passo
Por John Pires · 20 de março de 2026

DTOs type-safe em TypeScript: decorators, generics e type guards passo a passo
Por que aprender isso em 2026
TypeScript evoluiu para além de tipagem estática no editor: em 2026, aplicações modernas exigem translado seguro entre JSON não tipado (requests, bancos, APIs externas) e objetos fortemente tipados. DTOs (Data Transfer Objects) são a fronteira entre o mundo dinâmico e o modelo tipado da sua aplicação. Aprender a construir DTOs type-safe com decorators, generics e type guards reduz bugs, melhora autocompletar e torna validações consistentes.
Para iniciantes, a ideia central é simples: você descreve a forma dos dados em classes / tipos e cria código que verifica em tempo de execução se um objeto JSON respeita essa forma. Isso evita erros comuns como "campo ausente" ou "campo com tipo incorreto" antes que o erro chegue às camadas internas.
Em 2026, há duas razões práticas para adotar essa abordagem sem ferramentas pesadas: (1) ganho de produtividade com editor e intellisense; (2) menor dependência de bibliotecas externas quando você precisa de soluções sob medida. Vamos construir uma solução incremental que usa decorators para marcar campos, generics para mapear tipos, e type guards para validação.
Contexto técnico
Usaremos recursos do TypeScript atuais: decorators experimentais (com reflect-metadata quando necessário), generics, e funções de type guard. Nada de frameworks enormes — foco em APIs leves que você integra ao Express, Fastify, ou serverless.
O que você sairá sabendo
- Como anotar propriedades com decorators para construir metadados de DTOs.
- Como criar type guards reutilizáveis para validar primitivas e DTOs compostos.
- Como usar generics para mapear JSON para instâncias tipadas.
- Como integrar validação em um handler simples e tratar erros comuns.
Configuração do ambiente
Vamos montar um ambiente mínimo. Você precisa de Node.js (>=16 recomendado), npm/yarn, e TypeScript. Em projetos reais, adicione bundler/transpiler conforme necessário. Aqui usaremos ts-node para executar exemplos rápidos.
Passos rápidos
Crie uma pasta e inicialize:
mkdir dtos-demo
cd dtos-demo
npm init -y
npm install typescript ts-node reflect-metadata --save-dev
npx tsc --init
Explicação: o reflect-metadata é útil quando você quer capturar tipos em runtime via Reflect, e ts-node facilita rodar TypeScript sem build. Vamos ativar opções no tsconfig:
// tsconfig.json (trechos importantes)
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"esModuleInterop": true
}
}
Depois, crie um arquivo src/index.ts e importe reflect-metadata no topo:
// src/index.ts
import "reflect-metadata";
// seu código aqui
console.log('ambiente pronto');
Explicação: Reflect-metadata permite que decorators gravem e leiam metadados de tipos em tempo de execução. Não é mágico: o compilador precisa emitir metadata (emitDecoratorMetadata) para tipos básicos.
Notas de segurança e versão
Decorators ainda são considerados experimental em TS: use com cuidado e teste. Em 2026 a estabilidade é boa, mas mantenha dependencies atualizadas. Se preferir não usar reflect-metadata, é possível escrever decoradores que recebem explicitamente o tipo como string (ex: @Field('string')). Faremos ambos quando conveniente.
Hello World progressivo
Vamos construir incrementos. Primeiro, um decorator @Field que marca propriedades e registra o tipo desejado. Em seguida, um type guard para checar primitivas. Depois, uma função genérica mapToDto que cria uma instância da classe DTO com validação.
1) Decorator simples e metadados
O decorator vai anotar propriedades com o tipo pretendido. Para iniciantes, isso funciona como etiquetar caixas: você coloca um adesivo dizendo "isso deve ser string". O decorator grava o adesivo em um objeto interno.
Exemplo: decorador que armazena o tipo como string (sem reflect-metadata).
// src/decorators.ts
export const METADATA_FIELDS = Symbol('metadata_fields');
export type FieldType = 'string' | 'number' | 'boolean' | 'date' | 'object' | 'array';
export function Field(type: FieldType) {
return function (target: any, propertyKey: string) {
const ctor = target.constructor;
const existing = Reflect.getOwnMetadata(METADATA_FIELDS, ctor) || {};
existing[propertyKey] = type;
Reflect.defineMetadata(METADATA_FIELDS, existing, ctor);
};
}
O que faz: registra no constructor os tipos declarados. Depois podemos ler Reflect.getOwnMetadata(METADATA_FIELDS, SomeDto). Após: usaremos essa tabela para validar entradas JSON.
2) Type guards básicos
Type guards são funções que em runtime declaram para o compilador "se isso retornar true, trate como tipo X". Eles validam primitivas e estruturam checagens compostas.
// src/guards.ts
export function isString(v: any): v is string {
return typeof v === 'string';
}
export function isNumber(v: any): v is number {
return typeof v === 'number' && !Number.isNaN(v);
}
export function isBoolean(v: any): v is boolean {
return typeof v === 'boolean';
}
O que faz: valida tipos primitivos. Depois usaremos essas funções dentro do mapeador genérico para garantir que o valor JSON bate com o tipo anotado.
3) Mapeador genérico: mapToDto
Agora unimos tudo: função genérica que recebe um objeto qualquer e uma classe DTO (constructor). Ela cria uma instância da classe, valida e atribui propriedades, retornando a instância ou lançando erro.
// src/mapper.ts
import { METADATA_FIELDS, FieldType } from './decorators';
import { isString, isNumber, isBoolean } from './guards';
export function mapToDto<T>(ctor: new () => T, obj: any): T {
if (obj == null || typeof obj !== 'object') {
throw new Error('source must be an object');
}
const dto = new ctor();
const meta: Record<string, FieldType> = Reflect.getOwnMetadata(METADATA_FIELDS, ctor) || {};
for (const key of Object.keys(meta)) {
const expected = meta[key];
const val = obj[key];
if (val === undefined) {
throw new Error(`missing required field: ${key}`);
}
switch (expected) {
case 'string':
if (!isString(val)) throw new Error(`field ${key} must be string`);
(dto as any)[key] = val;
break;
case 'number':
if (!isNumber(val)) throw new Error(`field ${key} must be number`);
(dto as any)[key] = val;
break;
case 'boolean':
if (!isBoolean(val)) throw new Error(`field ${key} must be boolean`);
(dto as any)[key] = val;
break;
case 'date':
const d = new Date(val);
if (Number.isNaN(d.getTime())) throw new Error(`field ${key} must be valid date`);
(dto as any)[key] = d;
break;
default:
(dto as any)[key] = val; // para object/array deixamos passar (poderíamos validar recursivamente)
}
}
return dto;
}
O que faz: cria a instância tipada e valida cada campo anotado. Depois: use no handler de entrada para transformar JSON em DTO.
Mini-projeto prático
Objetivo: criar um endpoint simulado que recebe um corpo JSON para criar um usuário, valida via DTO, e retorna objeto tipado. Faremos em puro Node com uma função handleRequest simulando um request/response.
1) DTO do usuário
Definimos a classe UserDto com @Field em cada propriedade. Íntegraremos nossa função mapToDto para construir a instância.
// src/user-dto.ts
import { Field } from './decorators';
export class UserDto {
@Field('string')
public name!: string;
@Field('string')
public email!: string;
@Field('number')
public age!: number;
@Field('boolean')
public active!: boolean;
}
O que faz: descreve o formato esperado do JSON para criar usuário. Depois: passamos o JSON para mapToDto(UserDto, body) para obter a instância validada.
2) Handler e execução
Vamos implementar um handler simples que recebe uma string JSON, tenta parsear e mapear para UserDto. Tratamos erros para retornar mensagens amigáveis.
// src/handler.ts
import { mapToDto } from './mapper';
import { UserDto } from './user-dto';
export function handleCreateUser(jsonBody: string) {
try {
const parsed = JSON.parse(jsonBody);
const user = mapToDto(UserDto, parsed);
// Simula salvar no banco e retornar resposta
return { status: 201, body: { success: true, user } };
} catch (err: any) {
return { status: 400, body: { success: false, error: err.message } };
}
}
// Exemplo de uso:
const good = JSON.stringify({ name: 'Ana', email: 'ana@example.com', age: 30, active: true });
console.log(handleCreateUser(good));
const bad = JSON.stringify({ name: 'Bruno', email: 'bruno@example.com', age: 'trinta', active: true });
console.log(handleCreateUser(bad));
O que faz: demonstra sucesso e erro na validação. Depois: ao rodar via ts-node, você verá o primeiro retorno com user tipado e o segundo com mensagem de erro "field age must be number".
3) Validação recursiva e arrays
Para DTOs aninhados ou arrays, expanda o decorador para aceitar uma referência de classe (constructor) em vez de apenas 'object'. Isso permite chamar mapToDto recursivamente. Exemplo simplificado abaixo.
// src/decorators-advanced.ts
export function FieldTypeOf(type: 'primitive' | 'class' | 'array', meta?: any) {
return function (target: any, propertyKey: string) {
const ctor = target.constructor;
const existing = Reflect.getOwnMetadata(METADATA_FIELDS, ctor) || {};
existing[propertyKey] = { kind: type, meta };
Reflect.defineMetadata(METADATA_FIELDS, existing, ctor);
};
}
O que faz: estrutura metadados que permitem saber se um campo é um DTO (class) e qual classe instanciar, ou um array de DTOs. Depois: o mapper pode chamar mapToDto recursivamente.
Erros comuns, armadilhas e dicas práticas
Essa seção reúne problemas que você encontrará ao implementar DTOs type-safe pela primeira vez. Entender essas armadilhas evita horas de debug.
1) Decorators não funcionam porque emitDecoratorMetadata está off
Se você usar reflect-metadata e esperar tipos automaticamente, o tsconfig precisa de experimentalDecorators: true e emitDecoratorMetadata: true. Sem isso, Reflect.getMetadata não trará os tipos esperados.
2) Erros confusos por diferenças entre runtime e compile-time
Tipos do TypeScript existem apenas em tempo de compilação; em runtime você manipula valores JS. Não confunda uma assinatura de função com garantia de runtime. Por isso usamos type guards para checagem.
Exemplo de armadilha: uma propriedade marcada como number pode chegar como string '123'. Sem validação, atribuiria e causaria bugs laterais.
Tip: prefira falhar rápido. Lance erros claros no mapper ao invés de tentar coerções silenciosas que mascaram dados inválidos.
3) Arrays e objetos aninhados
Validação de arrays e objetos requer recursão. Se seu decorador só registra "object", você perderá validação de forma. Registre o tipo de item do array ou a classe do objeto aninhado para validar corretamente.
4) Performance e sobrecarga
Validar tudo em cada request tem custo. Balanceie entre validar campos críticos e confiar em camadas internas. Em endpoints de alto throughput, meça e otimize: cache de metadados, validações parciais, e desativação seletiva podem reduzir latência.
Recursos para continuar estudando
Depois deste passo a passo, aprofunde em tópicos que complementam: bibliotecas de validação (zod, io-ts), padrões DTO vs Entity, e integração com frameworks web. Cada opção tem trade-offs entre ergonomia, performance e complexidade.
Bibliotecas e leituras
- Zod: schemas polimórficos e parsing com inferência de tipo.
- io-ts: validação funcional e codecs, integra bem com fp-ts.
- class-transformer / class-validator: solução clássica baseada em decorators (mas com sobrecarga).
- Documentação oficial TypeScript sobre decorators e reflect-metadata.
O que praticar a seguir
- Adicione validação recursiva para DTOs aninhados e arrays.
- Implemente mensagens de erro estruturadas (caminho do campo, tipo esperado, valor recebido).
- Integre com testes unitários: casos positivos e negativos para garantir regressões não aconteçam.
Próximo passo: compare sua solução caseira com zod/io-ts em um benchmark e escolha em função de ergonomia e requisitos do time.
Conclusão prática
Você aprendeu um fluxo prático para transformar JSON em DTOs type-safe usando decorators, generics e type guards. A estratégia é incremental: comece marcando campos com @Field, crie type guards para primitivas, e um mapper genérico que valida e instancia DTOs. Depois expanda para validação recursiva, arrays, e mensagens de erro claras.
A vantagem desse caminho é o controle: você sabe exatamente o que é verificado e como — sem mágica. Para iniciantes, é uma ótima forma de reforçar entendimento sobre runtime vs compile-time e ganhar confiança com TypeScript avançado.
Implementação prática: use os snippets fornecidos como base. Teste com vários payloads, escreva testes unitários e experimente integrar zod para ver as diferenças.
Resumo prático: decorators = marcação; generics = mapeamento tipado; type guards = validação runtime. Juntos, reduzem erros e tornam seu código previsível e fácil de refatorar.
Pronto para o próximo passo: transforme essa base em uma biblioteca interna do seu time e padronize DTOs em todos os serviços.
Assuntos: TypeScript, dtos, decorators, type-guards, generics
Leia também
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…
IDs nominais em TypeScript: branded types, type guards e generics passo a passo
Aprenda a proteger IDs em TypeScript com branded types, type guards e generics. Do jeito errado (string genérico) ao jeito certo (fábrica + validação), com…
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…
Sistema de plugins type-safe em TypeScript com decorators e generics
Implementação prática de um sistema de plugins type-safe em TypeScript usando decorators, generics e type guards. Padrões, armadilhas comuns e exemplos reais…
Client HTTP type-safe com Fetch: decorators, generics e type guards
Padrão prático para construir um client HTTP type-safe com fetch usando decorators, generics e type guards: validação runtime, exemplos implementáveis e…