Phantom types: segurança em tempo de compilação

1. Introdução aos Phantom Types

Phantom types (tipos fantasmas) são uma técnica de programação onde um tipo genérico é declarado como parâmetro de um tipo, mas não é usado na representação em tempo de execução. Em TypeScript, isso significa que o parâmetro genérico existe apenas para o sistema de tipos — ele desaparece completamente durante a compilação, sem gerar qualquer overhead em runtime.

A diferença fundamental entre phantom types e tipos parametrizados convencionais é que, em tipos comuns como Array<string>, o parâmetro string influencia diretamente o comportamento em runtime. Já em phantom types como type Meter = number & { __brand: 'meter' }, a marca 'meter' existe apenas para o verificador de tipos.

Os benefícios são claros: segurança em tempo de compilação sem custo de runtime. Você obtém validações rigorosas que previnem bugs antes mesmo do código ser executado.

2. Problemas que Phantom Types Resolvem

Phantom types são particularmente úteis para resolver três categorias clássicas de problemas:

Unidades de medida: Misturar metros com quilômetros ou dólares com reais pode causar desastres financeiros ou de engenharia. O exemplo mais famoso é a perda da sonda Mars Climate Orbiter em 1999, onde a NASA usou unidades imperiais e a Lockheed Martin usou unidades métricas.

Máquinas de estado: Chamar métodos em estados inválidos — como tentar enviar dados por um socket desconectado ou autenticar uma conexão já autenticada.

Identificadores confusos: Passar um ID de usuário onde se espera um ID de produto, ou vice-versa. Em sistemas complexos, esses erros são comuns e difíceis de detectar.

3. Implementação Básica de Phantom Types em TypeScript

A implementação mais simples usa interseção de tipos com uma marca única:

type Meter = number & { __brand: 'meter' };
type Kilometer = number & { __brand: 'kilometer' };

// Funções de criação segura
const meters = (v: number): Meter => v as Meter;
const kilometers = (v: number): Kilometer => v as Kilometer;

// Uso
const distancia1: Meter = meters(100);
const distancia2: Kilometer = kilometers(5);

// Isso gera erro de compilação:
// const erro: Meter = kilometers(3); // Error: Type 'Kilometer' is not assignable to type 'Meter'

O as (type assertion) é necessário porque TypeScript não sabe que number pode ser tratado como nosso tipo fantasma. Isso é um ponto importante: a segurança vem do uso disciplinado das funções de criação.

4. Phantom Types para Unidades de Medida

Vamos construir um sistema de unidades fortemente tipado:

// Definição das unidades
type Unit = 'meter' | 'kilometer' | 'mile';

// Tipo fantasma principal
type Length<U extends Unit> = number & { __unit: U };

// Funções de criação
const meters = (v: number): Length<'meter'> => v as Length<'meter'>;
const kilometers = (v: number): Length<'kilometer'> => v as Length<'kilometer'>;
const miles = (v: number): Length<'mile'> => v as Length<'mile'>;

// Operações aritméticas seguras
function addLengths<U extends Unit>(a: Length<U>, b: Length<U>): Length<U> {
  return (a + b) as Length<U>;
}

// Conversão entre unidades
function toKilometers(m: Length<'meter'>): Length<'kilometer'> {
  return (m / 1000) as Length<'kilometer'>;
}

// Uso correto
const medida1 = meters(1500);
const medida2 = meters(200);
const soma = addLengths(medida1, medida2); // OK: 1700 metros

// Erro em tempo de compilação:
// const erro = addLengths(medida1, kilometers(3)); // Error!
// Só funciona após conversão explícita:
const convertido = toKilometers(medida1);
const somaValida = addLengths(convertido, kilometers(1.5)); // OK: 3 km

5. Phantom Types para Máquinas de Estado

Modelar estados de conexão com phantom types previne chamadas inválidas:

// Estados possíveis
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'authenticated';

// Tipo fantasma para conexão
type Connection<S extends ConnectionState> = {
  readonly host: string;
  readonly port: number;
  __state: S; // Phantom: não usado em runtime
};

// Funções de transição seguras
function createConnection(host: string, port: number): Connection<'disconnected'> {
  return { host, port } as Connection<'disconnected'>;
}

function connect(conn: Connection<'disconnected'>): Connection<'connecting'> {
  console.log(`Conectando a ${conn.host}:${conn.port}...`);
  return conn as Connection<'connecting'>;
}

function onConnected(conn: Connection<'connecting'>): Connection<'connected'> {
  console.log('Conexão estabelecida');
  return conn as Connection<'connected'>;
}

function authenticate(conn: Connection<'connected'>, token: string): Connection<'authenticated'> {
  console.log(`Autenticando com token ${token}...`);
  return conn as Connection<'authenticated'>;
}

function sendData(conn: Connection<'authenticated'>, data: string): void {
  console.log(`Enviando dados: ${data}`);
}

// Uso correto
const conn = createConnection('api.exemplo.com', 443);
const conectando = connect(conn);
const conectado = onConnected(conectando);
const autenticado = authenticate(conectado, 'token123');
sendData(autenticado, '{"mensagem": "olá"}');

// Erros em tempo de compilação:
// sendData(conectado, 'dados'); // Error: 'connected' não é 'authenticated'
// connect(conectado); // Error: 'connected' não é 'disconnected'

6. Phantom Types para Identificadores Seguros

Identificadores de diferentes entidades podem ser facilmente confundidos:

// Marcas únicas para cada tipo de ID
type UserId = string & { __brand: 'UserId' };
type ProductId = string & { __brand: 'ProductId' };
type OrderId = string & { __brand: 'OrderId' };

// Funções de criação
const createUserId = (id: string): UserId => id as UserId;
const createProductId = (id: string): ProductId => id as ProductId;
const createOrderId = (id: string): OrderId => id as OrderId;

// Funções tipadas
function getUser(id: UserId): { name: string } {
  return { name: `Usuário ${id}` };
}

function getProduct(id: ProductId): { title: string } {
  return { title: `Produto ${id}` };
}

function getOrder(id: OrderId): { total: number } {
  return { total: 100 };
}

// Uso seguro
const userId = createUserId('usr_123');
const productId = createProductId('prd_456');
const orderId = createOrderId('ord_789');

const user = getUser(userId); // OK
const product = getProduct(productId); // OK
const order = getOrder(orderId); // OK

// Erros em tempo de compilação:
// getUser(productId); // Error!
// getProduct(orderId); // Error!
// getOrder(userId); // Error!

7. Técnicas Avançadas e Padrões

Podemos compor phantom types para criar pipelines de validação:

// Estados de validação
type Raw = { __state: 'raw' };
type Validated = { __state: 'validated' };
type Sanitized = { __state: 'sanitized' };

// Dados que passam por múltiplas etapas
type UserInput<S> = {
  value: string;
  __state: S; // Phantom
};

// Funções de transformação
function validate(input: UserInput<Raw>): UserInput<Validated> {
  if (input.value.length < 3) throw new Error('Muito curto');
  return input as UserInput<Validated>;
}

function sanitize(input: UserInput<Validated>): UserInput<Sanitized> {
  const sanitized = input.value.replace(/<[^>]*>/g, '');
  return { value: sanitized } as UserInput<Sanitized>;
}

function saveToDatabase(input: UserInput<Sanitized>): void {
  console.log(`Salvando: ${input.value}`);
}

// Pipeline segura
const rawInput: UserInput<Raw> = { value: '<script>alert("xss")</script>' } as UserInput<Raw>;
const validated = validate(rawInput);
const sanitized = sanitize(validated);
saveToDatabase(sanitized);

// Erro: pular validação
// saveToDatabase(rawInput); // Error!

Limitações importantes:
- Type assertions (as) são necessárias e podem ser usadas indevidamente
- Bibliotecas externas ignoram suas marcas fantasma
- O sistema de tipos não impede conversões acidentais dentro da mesma função

8. Conclusão e Boas Práticas

Phantom types são uma ferramenta poderosa para adicionar segurança em tempo de compilação sem custo de runtime. Use-os quando:

  • Desenvolver APIs públicas onde erros de tipo são comuns
  • Trabalhar com domínios críticos (finanças, engenharia, saúde)
  • Criar código compartilhado entre equipes

Comparado a alternativas como enums ou classes, phantom types oferecem a melhor relação segurança/overhead. Enquanto enums são limitados a valores fixos e classes adicionam overhead de runtime, phantom types desaparecem completamente na compilação.

Boas práticas:
- Use nomes claros para as marcas (__brand, __unit, __state)
- Documente a intenção com comentários JSDoc
- Crie funções de criação seguras em vez de expor type assertions
- Considere usar bibliotecas como ts-brand ou branded-types para padronização

Lembre-se: phantom types não substituem testes ou validações em runtime. Eles são uma camada extra de segurança que pega erros antes mesmo do código ser executado.

Referências