Destino Dev

Blog

IDs nominais em TypeScript: branded types, type guards e generics passo a passo

Por John Pires · 13 de março de 2026

IDs nominais em TypeScript: branded types, type guards e generics

1) O que é e por que importa

IDs nominais são IDs que carregam um significado semântico além do valor primitivo. Em vez de usar simplesmente string ou number para qualquer identificador, um ID nominal garante que um UserId não seja passado acidentalmente para uma função que espera um PostId. Isso evita bugs sutis que aparecem em produção, especialmente em bases de código grandes.

Quando você trata IDs apenas como primitivas, ganha flexibilidade no curto prazo, mas perde segurança tipada. Em code review é comum ver: "aceita string e pronto". Esse é o jeito errado — a função aceita qualquer string e pode receber um ID do tipo errado. Em TypeScript podemos forçar restrições apenas em tempo de compilação usando branded types e type guards, e aplicar generics para criar fábricas reutilizáveis.

Resumo técnico rápido: branded types são um artifício de tipo (type-only) que adiciona uma marca ao tipo primitivo para diferenciá-lo. Type guards permitem validar em runtime que um valor pertence ao tipo nominal. Generics ajudam a criar funções e estruturas que trabalham com esses tipos sem repetir código.

Por que isso importa para iniciantes

Iniciantes frequentemente cometem confusão entre IDs de diferentes entidades. Um erro comum é passar um postId onde o backend esperava userId e só perceber no runtime. Usar IDs nominais evita essa classe de erro já no editor/CI.

Quando não usar

Se sua base de código é mínima ou IDs são efetivamente intercambiáveis, branded types adicionam complexo desnecessário. Use com parcimônia: quando tipos diferentes representam conceitos distintos, mesmo que ambos sejam strings.

2) Conceitos fundamentais com analogias

Analogia simples: imagine chaves físicas idênticas esteticamente, mas com etiquetas: "chave da casa" e "chave do carro". Ambas podem caber, mas só a etiqueta correta deve ser usada. Branded types são essas etiquetas no código: fisicamente ainda é uma string, mas o compilador vai reclamar se você tentar trocar etiquetas.

Outra analogia: caixas postais com etiquetas. A etiqueta não muda o objeto dentro, mas muda quem pode ler. Em TypeScript, o valor em runtime continua sendo string, mas o compilador obriga que você leia/acesse com a etiqueta correta.

Branded types — o que são tecnicamente

Um branded type é uma interseção entre um primitivo e um tipo literal que não existe em runtime. Por exemplo: type UserId = string & { __brand: 'UserId' }. No JavaScript transpilado, isso vira somente string; o "brand" é tipo-only e usado apenas pelo TypeScript.

Type guards — por que precisa validar em runtime

Branded types protegem em tempo de compilação, mas dado que em runtime tudo é string, quando você recebe dados externos (HTTP, JSON, localStorage) deve validar a forma e só então transformar numa unidade nominal. Type guard é a função que faz essa verificação e afirma para o compilador que o valor agora é do tipo nominal.

3) Passo a passo com código comentado

Vamos começar com o jeito errado (exemplo comum) e refatorar passo a passo para o jeito certo.

3.1 Jeito errado: IDs como string

Contexto: função simples que busca um registro por ID. Aqui equivocamos ao aceitar string genérico.

// Busca um usuário por ID (jeito errado, aceita qualquer string)
function getUserByIdWrong(id: string) {
  // imagine fetch -> backend
  return `fetched user for id ${id}`;
}

const postId = 'post-123';
// Erro sutil: passa postId para função que espera userId
const u = getUserByIdWrong(postId);
console.log(u);

O que acontece: o código compila e roda, mas logicamente estamos fazendo algo errado. Em produção isso vira bug: endpoint errado, dados inconsistentes.

3.2 Introduzindo branded types — o jeito certo

Contexto: vamos marcar IDs para que TypeScript não permita passar um tipo onde outro é esperado.

// Definição genérica de brand
type Brand<K, T> = K & { __brand: T };

// Tipos nominais para User e Post
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;

// Função que aceita apenas UserId
function getUserById(id: UserId) {
  return `fetched user for id ${id}`;
}

// Sintaxe curta para forçar cast (apenas quando você sabe o que faz)
const someUserId = 'user-1' as UserId;
getUserById(someUserId);

O que acontece: agora getUserById só aceita UserId. Passar um PostId gera erro em tempo de compilação. Atenção: cast direto (as UserId) é perigoso se o valor não for válido em runtime — por isso precisamos de validação.

3.3 Factory de IDs e validação com type guards

Contexto: vamos criar uma função reutilizável para construir e validar IDs nominais a partir de strings externas.

// Validador simples: exemplo baseada num prefixo
function isValidIdFormat(value: string, prefix: string): boolean {
  return value.startsWith(prefix + '-');
}

// Fábrica genérica para brand + type guard
function makeId<T extends string>(value: string, prefix: T) {
  if (!isValidIdFormat(value, prefix)) {
    throw new Error('invalid id format');
  }
  // cast seguro após validação
  return value as Brand<string, T>;
}

// Uso
const safeUserId = makeId('user-1', 'UserId');
const safePostId = makeId('post-9', 'PostId');
// getUserById(safePostId); // compila? nao: erro, tipos incompatíveis

O que acontece: makeId valida o formato em runtime e retorna um branded type para o compilador. Assim você ganha segurança tanto em runtime quanto em compile-time.

4) Exemplo prático completo

Contexto: vamos construir um mini-repositório tipado que lida com User e Post usando branded types, type guards e generics. Objetivo: evitar misturar IDs e fornecer helpers para mapear e buscar entidades.

Estrutura do exemplo:

  • Definições de tipos nominais
  • Fábricas e guards
  • Repositório genérico que aceita apenas IDs corretos

Tipos nominais e factories

Contexto: definimos os tipos e factories reutilizáveis.

// Tipos base
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;

// Validação simples por regex (padrão: entity-<num>)
const ID_REGEX = /^(user|post)-\d+$/;

function isUserId(x: unknown): x is UserId {
  return typeof x === 'string' && x.startsWith('user-') && ID_REGEX.test(x);
}

function isPostId(x: unknown): x is PostId {
  return typeof x === 'string' && x.startsWith('post-') && ID_REGEX.test(x);
}

function makeUserId(value: string): UserId {
  if (!isUserId(value)) throw new Error('invalid user id');
  return value;
}

function makePostId(value: string): PostId {
  if (!isPostId(value)) throw new Error('invalid post id');
  return value;
}

O que acontece: temos validações concretas e makers que garantem que apenas strings com formato correto tornar-se-ão IDs nominais.

Repositório genérico e uso

Contexto: implementamos funções tipadas para evitar erros de mistura de IDs.

// Entidades
type User = { id: UserId; name: string };
type Post = { id: PostId; authorId: UserId; title: string };

// Repositórios simples em memória com assinatura segura
const users = new Map<string, User>();
const posts = new Map<string, Post>();

function addUser(user: User) {
  users.set(user.id, user);
}

function addPost(post: Post) {
  if (!users.has(post.authorId)) throw new Error('author not exists');
  posts.set(post.id, post);
}

// Buscas seguras
function findUserById(id: UserId): User | undefined {
  return users.get(id);
}

function findPostById(id: PostId): Post | undefined {
  return posts.get(id);
}

// Exemplo de execução segura
const uId = makeUserId('user-1');
addUser({ id: uId, name: 'alice' });

const pId = makePostId('post-1');
addPost({ id: pId, authorId: uId, title: 'Hello' });

// findUserById(pId); // erro em compile-time: PostId nao é UserId

O que acontece: o TypeScript impede que você chame findUserById com um PostId. No runtime, as collections usam string como chave; os branded types existem apenas no compilador, então everything works.

Erros comuns e armadilhas

Contexto: lista de problemas que vejo em code reviews e como evitá-los.

  • Usar cast indiscriminado (as X): substitui a validação. Evite. Só cast quando você tem validação anterior.
  • Ignorar runtime validation: branded types não impedem dados inválidos virem de JSON. Sempre valide inputs.
  • Mixar técnicas: usar enums + branded types sem padrão confuso. Padronize a estratégia no repo.
Tip: prefira factories que validam e retornam branded types em vez de usar casts em todo lugar. Isso evita que o cast se espalhe pelo código.

Exemplo de armadilha: serializar e desserializar. Ao desserializar, você recebe string — precisa rodar makeUserId antes de usar como UserId.

5) Próximos passos para aprofundar

Depois de dominar branded types básicos e guards, avance nos seguintes pontos:

  1. Use zod ou io-ts para validações robustas e transformações automáticas para branded types.
  2. Combine branded types com unit tests que validem fracamente os borders (por exemplo, testes de integração que confirmem validação de IDs do backend).
  3. Explore utility types e mapped types para criar tipos nominais automaticamente a partir de entidades.

Exemplo avançado (esboço): usar zod para parsear e brandear automaticamente. Isso reduz boilerplate e garante mensagens de erro claras no runtime.

// Exemplo esquemático com zod (requere zod instalado)
// import { z } from 'zod';
// const UserIdSchema = z.string().regex(/^user-\d+$/).transform(s => s as UserId);
// const result = UserIdSchema.safeParse('user-10');
// if (result.success) { const uid = result.data; /* uid tem tipo UserId */ }

O que acontece: bibliotecas de runtime validation podem emitir tipos nominais automaticamente via transform. Boa prática para projetos maiores.

Conclusão prática

IDs nominais reduzem erros de mistura de identificadores e melhoram a segurança do tipo no editor. Padrão comum: criar uma pequena fábrica + type guard para cada entidade e usar um tipo Brand genérico. No code review, rejeite APIs que aceitam string genérico para IDs — peça branded types ou validação explícita.

Checklist rápido para introduzir IDs nominais em um projeto:

  • Defina Brand e tipos específicos (UserId, PostId).
  • Implemente makers/validators que criam esses IDs a partir de strings externas.
  • Substitua assinaturas que usam string por tipos nominais progressivamente.
  • Adote ferramentas de validação (zod/io-ts) para reduzir boilerplate.

Implementando esses passos você transforma um risco de runtime em garantias no compile-time e em validações claras no runtime. Em code review: peça a fábrica + guard sempre que encontrar "id: string" em APIs que deveriam ser nominadas.

Se precisar, forneço um repo exemplo com todos os trechos prontos e scripts npm para rodar os testes e ver o erro em micro-exemplos.

Assuntos: TypeScript, branded-types, type-guards, generics, best-practices

Leia também