🏗️ Tomas
typescriptnodejsarquiteturabackendtécnico

TypeScript além do básico: Padrões que uso em produção

10 min 26/05/2026

TypeScript além do básico: Padrões que uso em produção


TypeScript virou o padrão no ecossistema JavaScript por um motivo: ele pega bugs na hora da digitação, não em produção. Mas depois de 10 anos escrevendo TypeScript em empresas como Chipper Cash (fintech com milhões de usuários) e ThoughtWorks, percebi que a maioria dos devs usa só 20% do que a linguagem oferece.

Este artigo não é sobre tipos básicos. É sobre os padrões que realmente fazem diferença quando seu projeto cresce.

1. Discriminated Unions — Seu melhor amigo

Em vez de usar optional fields e tratar undefined em todo lugar, use discriminated unions:

// ❌ Ruim
type ApiResponse = {
  success: boolean;
  data?: User[];
  error?: string;
};

// ✅ Bom
type ApiResponse =
  | { status: 'success'; data: User[] }
  | { status: 'error'; code: number; message: string }
  | { status: 'loading' };

// O TypeScript estreita o tipo automaticamente
function handleResponse(res: ApiResponse) {
  switch (res.status) {
    case 'success':
      return renderUsers(res.data); // res.data é User[]
    case 'error':
      return showError(res.message); // res.message é string
    case 'loading':
      return showSpinner();
  }
}

Isso elimina if (data && error) e estados impossíveis de representar. Usei esse padrão extensivamente na Chipper Cash para modelar respostas de APIs financeiras — cada estado possível estava explícito no tipo.

2. Branded Types — Domínio no tipo

CPF, ID, Email, Slug são todos strings — até não serem. Um string não te impede de passar um email onde espera um CPF:

// ❌ Ruim
function getUser(id: string) {}
function getOrder(id: string) {}
getUser(getOrder('123')); // Compila, mas não faz sentido

// ✅ Bom
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function getUser(id: UserId) {}
function getOrder(id: OrderId) {}

getUser('abc' as UserId); // OK
getUser(getOrder('abc' as OrderId)); // ❌ Erro de tipo!

Na ThoughtWorks, usávamos isso pra modelar IDs de agregados em sistemas de banking — o compilador impedia misturar IDs de domínios diferentes.

3. Tratamento de erros sem try/catch

Try/catch aninhado é o maior gerador de código feio. Uma alternativa que funciona bem em produção é usar um tipo Result:

type Result<T, E = Error> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

async function divide(a: number, b: number): Promise<Result<number>> {
  if (b === 0) return { ok: false, error: new Error('Division by zero') };
  return { ok: true, value: a / b };
}

const result = await divide(10, 0);
if (!result.ok) {
  console.error(result.error.message);
  return;
}
console.log(result.value); // Tipo estreitado para number

Em projetos com múltiplas chamadas encadeadas (típico em integrações com APIs de terceiros), isso elimina o aninhamento de try/catch.

4. Type Guards customizados

Nem tudo dá pra expressar com tipos estáticos. Pra validação em runtime, use type guards:

interface User {
  id: string;
  email: string;
  role: 'admin' | 'user';
}

function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    typeof (obj as any).id === 'string' &&
    typeof (obj as any).email === 'string' &&
    ['admin', 'user'].includes((obj as any).role)
  );
}

// Uso: o TypeScript sabe que data é User após o guard
fetch('/api/user')
  .then(res => res.json())
  .then((data: unknown) => {
    if (isUser(data)) {
      console.log(data.email); // ✅ type-safe
    }
  });

5. Estrutura de projetos que escalam

Depois de anos ajustando arquiteturas, cheguei numa estrutura que funciona do time pequeno ao grande:

src/
  shared/          # Tipos, utils, constantes (SEM dependências internas)
    types/
    utils/         
  domain/          # Lógica de negócio (regras PURAS, sem efeito colateral)
    user/
    order/
  services/        # Integrações externas (APIs, bancos, filas)
  http/            # Controllers, middlewares, rotas
    routes/
    middlewares/
  infra/            # Database, cache, config

Regra de ouro: shared não importa domain, domain não importa infra. Só services e http fazem a ponte. Isso te permite testar domain com mocking zero.


Sobre a Haruo: Haruo é uma software house full-stack especializada em produtos web, APIs e automação com IA. haruo.dev


Quer elevar a qualidade do código TypeScript da sua equipe? Fale com a Haruo →

Quer levar isso para produção?

Na Haruo, implementamos agents de IA, automações e sistemas escaláveis para empresas que querem resultados reais. Vamos conversar sobre seu projeto.

Falar com a Haruo →