1

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:

AbordagemTempo mediano
Projeção + proteção convencional44,83 ms
Projeção + TTOptImmut10,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?

Obrigado a quem dedicar um tempinho para conhecer o projeto!

Carregando publicação patrocinada...