Pitch: TypeScript com validação real em runtime: TS Typed (Open Source)
Sempre gostei bastante de trabalhar com TypeScript, no backend principalmente com NestJS. Porém, existe algo que sempre me incomodou: a necessidade de escrever validações separadas para garantir que os dados realmente correspondam aos tipos declarados.
Foi tentando resolver esse problema que comecei a desenvolver o TS Typed, uma biblioteca open source que adiciona validação de tipos em tempo de execução a projetos TypeScript, incluindo aplicações NestJS.
Importante: o TS Typed está em fase Alpha, ainda muito inicial, com funcionalidades em implementação e testes. Não recomendo seu uso em produção neste momento. Estou compartilhando o projeto para receber feedback, encontrar problemas e buscar contribuições da comunidade.
O problema que me motivou
Sempre que desenvolvo um backend, normalmente utilizo o NestJS. Gosto bastante da estrutura e das funcionalidades que o framework oferece.
Para validação dos DTOs, costumo utilizar class-validator, class-transformer e ValidationPipe.
Essas ferramentas funcionam bem, mas algo sempre me incomodou: precisamos manter a tipagem do TypeScript e as regras dos decorators sincronizadas.
Por exemplo:
export class CreateUserDto {
@IsString()
age: number;
}
O TypeScript espera um número, mas o decorator está validando uma string. São duas definições que podem entrar em conflito.
Além disso, sabemos que os tipos do TypeScript desaparecem durante a compilação. Nada impede que um código como este seja executado:
let name: string = 'Patrick';
name = 123 as unknown as string;
Depois da compilação, o JavaScript aceita essa atribuição normalmente.
Também existem outras situações, como propriedades private e readonly do TypeScript que nem sempre possuem a proteção esperada em runtime, retornos de funções com valores inesperados e objetos recebidos de fontes externas sem a estrutura declarada.
O TypeScript ajuda muito durante o desenvolvimento, mas não garante sozinho que os tipos serão respeitados durante a execução.
Como surgiu o TS Typed?
Na verdade, comecei esse projeto há alguns anos, antes da popularização das ferramentas de IA.
A ideia era fazer o JavaScript respeitar os contratos definidos no TypeScript, mas a implementação ficou bastante complexa. Por falta de tempo, acabei abandonando o projeto.
Com a evolução das IAs para auxiliar no desenvolvimento, resolvi retomar a ideia e comecei a construir o TS Typed.
A proposta é simples de explicar:
Usar os próprios tipos do TypeScript para gerar validações em runtime, sem precisar duplicar essas definições em schemas ou decorators.
Para isso, a biblioteca utiliza um compilador que analisa os tipos do projeto e insere verificações no JavaScript gerado.
Assim, dentro das operações suportadas, caso um valor não corresponda ao tipo esperado, a biblioteca lança um StrictTypeError.
E isso não fica restrito aos DTOs: a proposta também envolve propriedades de classes, parâmetros, variáveis, retornos de funções e outras estruturas do TypeScript.
Exemplo prático com NestJS
Além da validação de tipos, quis aproveitar para adicionar recursos que facilitassem o desenvolvimento de aplicações, principalmente com dados comuns no Brasil.
Por isso, criei validadores para CPF, CNPJ, CEP, CNH, e-mail, nome completo e outros.
Um DTO de cadastro de usuário pode ficar assim:
import { CPF, Email, FullName } from '@ts-typed/validators';
import { TypeErrorResponse } from '@ts-typed/nestjs';
export class CreateUserDto {
fullName!: FullName;
@TypeErrorResponse('CPF inválido')
cpf!: CPF;
email!: Email;
age!: number;
}
Sem precisar adicionar @IsString(), @IsEmail(), @IsNumber() ou implementar manualmente a validação do CPF.
O @TypeErrorResponse() é opcional e serve apenas para personalizar a mensagem de erro.
Com o código instrumentado pelo TS Typed, os valores recebidos nos campos CPF, Email e FullName podem ser automaticamente transformados em objetos validados e possuir métodos próprios que facilitam o uso, por exemplo:
body.fullName.firstName(); // Primeiro nome
body.fullName.lastName(); // Sobrenome
console.log(body.fullName); // Primeiro nome Sobrenome (string exatamente como foi enviada)
console.log(body.cpf); // 12345678909 (string exatamente como foi enviada)
body.cpf.masked(); // 123.456.789-09
body.cpf.numbers(); // 12345678909
body.cpf.redact(); // ***.456.789-**
O redact() é útil para reduzir a exposição de informações pessoais em determinadas respostas e logs. Naturalmente, apenas mascarar os dados não garante conformidade com a LGPD.
No controller, o uso continua simples:
@Post()
create(@Body() body: CreateUserDto) {
return this.userService.create(body);
}
A intenção é que dados inválidos sejam identificados na entrada do método, antes de prosseguir para o service.
A integração também oferece o StrictTypeErrorFilter, que pode ser registrado globalmente no NestJS para transformar erros de tipo em respostas HTTP adequadas.
Tudo isso depende de compilar a aplicação utilizando o TS Typed. A biblioteca não altera magicamente o comportamento de um build TypeScript convencional.
E a performance?
Esse é um dos pontos que mais me preocupam, já que adicionar validações em runtime inevitavelmente possui um custo.
Pensando nisso, implementei uma otimização chamada TTOptImmut<T>.
Ela permite validar e tornar profundamente imutáveis objetos JavaScript simples e arrays novos, evitando a instalação de getters e setters individuais em cada propriedade.
Por exemplo:
import type { TTOptImmut } from '@ts-typed/compiler';
interface UserResponse {
id: number;
name: string;
email: string;
}
type UsersResponse = TTOptImmut<UserResponse[]>;
Dessa forma, tanto o array quanto seus objetos ficam protegidos contra alterações após a validação.
Em um benchmark local com 10.000 respostas, obtive os seguintes resultados:
| Abordagem | Tempo mediano |
|---|---|
| Projeção + proteção convencional | 44,83 ms |
Projeção + TTOptImmut | 10,58 ms |
Nesse cenário específico, a otimização foi aproximadamente 4,2 vezes mais rápida.
É importante deixar claro que o TTOptImmut possui restrições e não foi desenvolvido para qualquer estrutura, como entities, instâncias de classes, Map e Set. Os números também não representam uma garantia de desempenho em aplicações reais.
Ainda estou trabalhando em melhorias de performance, compatibilidade e cobertura de testes.
Qual é o objetivo?
Não quero substituir o TypeScript ou dizer que bibliotecas como class-validator são ruins. Elas resolvem problemas reais e são bastante consolidadas.
Minha ideia é experimentar uma abordagem diferente: escrever os tipos uma única vez e aproveitar esses mesmos contratos tanto durante o desenvolvimento quanto em runtime.
Também quero reduzir a quantidade de configurações repetitivas e evitar situações nas quais as validações acabam sendo esquecidas ou configuradas incorretamente.
Sei que não é possível proteger absolutamente todos os comportamentos dinâmicos do JavaScript, mas acredito que seja possível melhorar bastante a segurança dos contratos de tipos sem sacrificar a experiência de desenvolvimento.
O projeto é open source e gostaria de feedback!
O TS Typed ainda está muito no início. Existem funcionalidades que precisam amadurecer, limitações conhecidas e muitos cenários que ainda precisam ser testados.
Por isso, não utilizem em produção neste momento. A ideia é experimentar em ambientes de desenvolvimento e ajudar o projeto a evoluir.
Se alguém tiver interesse, qualquer contribuição será muito bem-vinda: reportar bugs, sugerir funcionalidades, adicionar testes, implementar novos validadores ou abrir Pull Requests.
Também gostaria muito de ouvir a opinião de vocês:
Acham que essa abordagem faz sentido? Quais problemas ou dificuldades vocês enxergam em tentar garantir os contratos do TypeScript em runtime?
- GitHub: https://github.com/ts-typed
- Documentação: https://www.ts-typed.com
- Otimizações e benchmarks: https://www.ts-typed.com/pt/docs/0.1/optimizations
Obrigado a quem dedicar um tempinho para conhecer o projeto!
Fonte: https://www.ts-typed.com