Entendendo Metadata e Reflect-Metadata
Metadata é simplesmente informação sobre informação. Em TypeScript, trata-se de dados que descrevem características de classes, métodos, propriedades e parâmetros, sem fazer parte da lógica principal do programa. Quando você escreve uma classe com tipos específicos, o TypeScript remove toda essa informação de tipo durante a compilação para JavaScript — porque JavaScript não tem tipos nativos. O reflect-metadata é uma biblioteca que recupera e armazena essa informação em tempo de execução, permitindo que você a consulte dinamicamente.
O reflect-metadata implementa o padrão de reflexão do JavaScript (parte da proposta TC39) e funciona através de uma API global chamada Reflect. Esta biblioteca é essencial quando você precisa fazer coisas como serialização automática, injeção de dependência, validação de dados ou criação de ORMs. Sem ela, seria extremamente complexo descobrir quais são os tipos de propriedades de uma classe em tempo de execução — especialmente em cenários avançados com decorators.
Decorators: O Mecanismo de Injeção de Metadata
O que são Decorators
Decorators são funções que modificam ou anotam classes, métodos, propriedades e parâmetros. Eles são executados em tempo de compilação (ou mais precisamente, quando a classe é definida) e podem injetar metadata que será consultada depois. Um decorator é essencialmente uma função que recebe um alvo e retorna uma versão modificada dele.
Para usar decorators em TypeScript, você precisa ativar a opção experimentalDecorators no arquivo tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": ["ES2020"],
"strict": true
}
}
A opção emitDecoratorMetadata é crucial — ela faz o TypeScript compilar automaticamente metadata de tipos nas classes que usam decorators.
Decorators de Classe
Um decorator de classe recebe o construtor da classe como argumento e pode modificá-lo ou envolver-o. Vamos ver um exemplo prático:
import 'reflect-metadata';
function Serializable(): ClassDecorator {
return function (constructor: Function) {
// Armazena metadata indicando que esta classe é serializável
Reflect.defineMetadata('serializable', true, constructor);
};
}
@Serializable()
class Usuario {
nome: string = 'João';
idade: number = 30;
}
const usuario = new Usuario();
const ehSerializavel = Reflect.getMetadata('serializable', Usuario);
console.log(ehSerializavel); // true
Neste exemplo, o decorator @Serializable() marca a classe Usuario como serializável armazenando um booleano na metadata da classe. Quando você consulta com Reflect.getMetadata(), recupera essa informação.
Decorators de Propriedade
Decorators de propriedade recebem o protótipo do objeto (ou o construtor, se for propriedade estática) e a chave da propriedade. Eles são úteis para validação ou transformação de dados:
import 'reflect-metadata';
function Validar(tipo: Function): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol | undefined) {
const metadataTypes = Reflect.getOwnMetadata('design:type', target, propertyKey) || [];
Reflect.defineMetadata('validation:type', tipo, target, propertyKey);
};
}
class Produto {
@Validar(String)
nome: string;
@Validar(Number)
preco: number;
constructor(nome: string, preco: number) {
this.nome = nome;
this.preco = preco;
}
}
const produto = new Produto('Notebook', 3000);
const tipoValidacao = Reflect.getMetadata('validation:type', produto, 'nome');
console.log(tipoValidacao === String); // true
Aqui o decorator @Validar() armazena o tipo esperado de cada propriedade, permitindo validações posteriores.
Decorators de Método
Decorators de método recebem o protótipo, o nome do método e o descritor de propriedade. São frequentemente usados para logging, caching ou controle de acesso:
import 'reflect-metadata';
function Log(): MethodDecorator {
return function (target: Object, propertyKey: string | symbol | undefined, descriptor: PropertyDescriptor) {
const metodoOriginal = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Chamando ${String(propertyKey)} com argumentos:`, args);
const resultado = metodoOriginal.apply(this, args);
console.log(`${String(propertyKey)} retornou:`, resultado);
return resultado;
};
return descriptor;
};
}
class Calculadora {
@Log()
somar(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculadora();
calc.somar(5, 3);
// Saída:
// Chamando somar com argumentos: [5, 3]
// somar retornou: 8
Reflect-Metadata: Consultando e Manipulando Metadata
Armazenando Metadata com Reflect
O módulo reflect-metadata fornece uma API para armazenar e recuperar metadata. Os métodos principais são defineMetadata(), getMetadata(), getOwnMetadata() e hasMetadata(). A diferença entre getMetadata() e getOwnMetadata() é que o primeiro procura na cadeia de herança, enquanto o segundo busca apenas na metadata definida diretamente no alvo.
import 'reflect-metadata';
class Animal {}
class Cachorro extends Animal {}
// Define metadata na classe Animal
Reflect.defineMetadata('tipo', 'Animal', Animal);
// Consultando
console.log(Reflect.getMetadata('tipo', Animal)); // "Animal"
console.log(Reflect.getMetadata('tipo', Cachorro)); // "Animal" (herança)
console.log(Reflect.getOwnMetadata('tipo', Cachorro)); // undefined (não foi definida diretamente em Cachorro)
Exemplo Prático: Construindo um Sistema de Validação
Vamos criar um validador simples que usa metadata para validar objetos:
import 'reflect-metadata';
function IsString(): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol | undefined) {
Reflect.defineMetadata('validation:type', 'string', target, propertyKey);
};
}
function IsNumber(): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol | undefined) {
Reflect.defineMetadata('validation:type', 'number', target, propertyKey);
};
}
function ValidarObjeto(objeto: any): boolean {
const propiedades = Object.getOwnPropertyNames(objeto);
for (const prop of propiedades) {
const tipoEsperado = Reflect.getMetadata('validation:type', objeto, prop);
if (tipoEsperado) {
const tipoReal = typeof objeto[prop];
if (tipoReal !== tipoEsperado) {
console.error(`Propriedade "${prop}" deveria ser ${tipoEsperado}, mas é ${tipoReal}`);
return false;
}
}
}
return true;
}
class Pessoa {
@IsString()
nome: string;
@IsNumber()
idade: number;
constructor(nome: string, idade: number) {
this.nome = nome;
this.idade = idade;
}
}
const pessoa1 = new Pessoa('Maria', 28);
console.log(ValidarObjeto(pessoa1)); // true
const pessoa2 = new Pessoa('João', '30' as any);
console.log(ValidarObjeto(pessoa2)); // false e exibe erro
Acessando Metadata de Tipos com Design:Type
Quando você ativa emitDecoratorMetadata no TypeScript, a compilação automaticamente injeta metadata especial chamada design:type, design:paramtypes e design:returntype. Estas contêm os tipos das propriedades e parâmetros:
import 'reflect-metadata';
function Inspecionar(): ClassDecorator {
return function (constructor: Function) {
const propiedades = Object.getOwnPropertyNames(constructor.prototype);
for (const prop of propiedades) {
const tipo = Reflect.getMetadata('design:type', constructor.prototype, prop);
if (tipo) {
console.log(`${prop}: ${tipo.name}`);
}
}
};
}
@Inspecionar()
class Veiculo {
marca: string;
ano: number;
ativo: boolean;
}
// Saída:
// marca: String
// ano: Number
// ativo: Boolean
O TypeScript injeta automaticamente a metadata design:type em cada propriedade decorada. Isso permite que você inspecione tipos em tempo de execução, algo impossível de fazer de outra forma em JavaScript puro.
Caso de Uso Real: ORM Simplificado com Decorators e Metadata
Estrutura Básica
Vamos construir um mini-ORM que simula operações de banco de dados usando metadata para mapear propriedades de classes para colunas de tabelas:
import 'reflect-metadata';
interface ColumnOptions {
name?: string;
type?: string;
}
function Column(opcoes?: ColumnOptions): PropertyDecorator {
return function (target: Object, propertyKey: string | symbol | undefined) {
const colunaNome = opcoes?.name || String(propertyKey);
const tipoDb = opcoes?.type || 'TEXT';
Reflect.defineMetadata('column:name', colunaNome, target, propertyKey);
Reflect.defineMetadata('column:type', tipoDb, target, propertyKey);
};
}
function Tabela(nomeDaTabela: string): ClassDecorator {
return function (constructor: Function) {
Reflect.defineMetadata('table:name', nomeDaTabela, constructor);
};
}
@Tabela('usuarios')
class Usuario {
@Column({ name: 'user_id', type: 'INTEGER' })
id: number;
@Column({ name: 'user_name', type: 'VARCHAR' })
nome: string;
@Column({ name: 'user_email', type: 'VARCHAR' })
email: string;
constructor(id: number, nome: string, email: string) {
this.id = id;
this.nome = nome;
this.email = email;
}
}
Gerador de SQL Automático
Com a metadata armazenada, podemos gerar SQL dinamicamente:
function GerarCreateTable(classe: Function): string {
const nomeDaTabela = Reflect.getMetadata('table:name', classe);
const propiedades = Object.getOwnPropertyNames(classe.prototype);
const colunas = propiedades
.filter(prop => Reflect.hasMetadata('column:name', classe.prototype, prop))
.map(prop => {
const colunaNome = Reflect.getMetadata('column:name', classe.prototype, prop);
const tipoDb = Reflect.getMetadata('column:type', classe.prototype, prop);
return `${colunaNome} ${tipoDb}`;
})
.join(', ');
return `CREATE TABLE ${nomeDaTabela} (${colunas});`;
}
console.log(GerarCreateTable(Usuario));
// Saída: CREATE TABLE usuarios (user_id INTEGER, user_name VARCHAR, user_email VARCHAR);
Persistência Simulada
Podemos criar um repositório que automaticamente serializa e deserializa objetos baseado na metadata:
class Repositorio<T> {
private dados: Map<number, any> = new Map();
private proximoId: number = 1;
salvar(entidade: T): T {
const id = this.proximoId++;
const nomeDaTabela = Reflect.getMetadata('table:name', entidade.constructor);
const propiedades = Object.getOwnPropertyNames(entidade.constructor.prototype);
const registro: any = { id };
for (const prop of propiedades) {
if (Reflect.hasMetadata('column:name', entidade.constructor.prototype, prop)) {
const colunaNome = Reflect.getMetadata('column:name', entidade.constructor.prototype, prop);
registro[colunaNome] = (entidade as any)[prop];
}
}
this.dados.set(id, registro);
console.log(`Salvo em ${nomeDaTabela}:`, registro);
return entidade;
}
obter(id: number): any {
return this.dados.get(id);
}
}
const repo = new Repositorio<Usuario>();
const usuario = new Usuario(1, 'Carlos', 'carlos@email.com');
repo.salvar(usuario);
console.log(repo.obter(1));
// Saída: { id: 1, user_id: 1, user_name: 'Carlos', user_email: 'carlos@email.com' }
Conclusão
Aprendemos que metadata e decorators trabalham em conjunto para adicionar camadas de introspection e automação sem comprometer a clareza do código. Decorators são o mecanismo de injeção, enquanto reflect-metadata é a API que permite consultar essa informação posteriormente. Este padrão é tão poderoso que frameworks como NestJS, TypeORM e class-validator construem suas arquiteturas inteiras sobre ele.
A segunda grande lição é que TypeScript com emitDecoratorMetadata habilitado recupera informações de tipo que seriam perdidas na compilação, permitindo validações, transformações e mapeamentos dinâmicos que um JavaScript puro nunca conseguiria fazer. Isso transforma TypeScript de simplesmente "um JavaScript com tipos" em uma linguagem com capacidades de metaprogramação.