TypeScript satisfies: Type Safety sem Widening de Tipos

Aprenda a usar o operador satisfies do TypeScript 4.9 para validar tipos complexos sem perder inferência precisa. Guia completo com exemplos práticos.

TypeScript satisfies: Validação de Tipos sem Perder Inferência Precisa

Configurações complexas em TypeScript sempre foram um campo minado entre duas escolhas ruins: ou você anota tipos explicitamente e perde inferência precisa nas propriedades individuais, ou mantém a inferência mas abre mão de validação estrutural. O operador satisfies, introduzido no TypeScript 4.9 em novembro de 2022, resolve esse dilema de forma elegante.

O problema atinge diretamente quem trabalha com objetos de configuração, routing tables, theme systems ou qualquer estrutura onde você precisa garantir conformidade com um schema sem sacrificar a inteligência do compilador sobre valores específicos. Antes do satisfies, você escolhia entre segurança e precisão.

O Problema Fundamental: Type Widening vs Type Safety

Quando você anota um tipo explicitamente com :, o TypeScript faz type widening. Ele generaliza os valores para o tipo mais amplo compatível, perdendo informação sobre literais específicos.

Considere um objeto de configuração de rotas:

type Route = {
  path: string;
  method: string;
};

const routes: Record<string, Route> = {
  home: { path: "/", method: "GET" },
  createUser: { path: "/users", method: "POST" }
};

// Type widening aconteceu:
routes.home.method // tipo inferido: string
// Você perdeu a informação que é especificamente "GET"

O autocomplete agora oferece todas as keys de string possíveis, não apenas “home” e “createUser”. O tipo do método é string, não o literal "GET". Você tem validação estrutural (o objeto precisa ter path e method), mas perdeu precisão.

A tentação é remover a anotação de tipo para recuperar a inferência:

const routes = {
  home: { path: "/", method: "GET" },
  createUser: { path: "/users", method: "POST" }
};

Agora você tem inferência precisa, mas zero validação. Se adicionar uma rota malformada, o compilador não reclama. Propriedades extras passam despercebidas.

As Três Abordagens: Diferenças Fundamentais

O ecossistema TypeScript oferece três ferramentas que aparentemente fazem coisas similares, mas têm comportamentos muito diferentes.

Type Annotations (:) causam widening e permitem propriedades extras sem erro. O compilador valida que o valor é compatível com o tipo, mas não que é exatamente aquele tipo. Essa permissividade estrutural às vezes é desejada, mas geralmente é uma fonte de bugs silenciosos.

Const Assertions (as const) vão para o extremo oposto: produzem tipos readonly com valores literais estritos. Cada string vira seu literal específico, cada array vira tupla, cada propriedade vira readonly. Poderoso para objetos completamente imutáveis, mas excessivo quando você precisa de alguma flexibilidade.

O operador satisfies valida conformidade em compile-time sem alterar o tipo inferido. Pense nele como um teste de compatibilidade que não deixa rastro na inferência final. O compilador verifica se o valor atende ao tipo especificado (incluindo detecção de propriedades extras), mas mantém a inferência natural do valor.

A diferença prática é significativa. Com type annotation, você está dizendo “trate isso como tipo X”. Com as const, você está dizendo “congele tudo em literais readonly”. Com satisfies, você está dizendo “garanta que isso é compatível com X, mas mantenha o que você inferiu naturalmente”.

Validação Estrutural com Inferência Intacta

O operador satisfies foi implementado via PR #46827 no repositório microsoft/TypeScript, e sua sintaxe é deliberadamente simples: expression satisfies Type. O compilador executa uma verificação de compatibilidade completa (incluindo excess property checking), mas o tipo final da expressão permanece o que o TypeScript naturalmente inferiria.

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3
} satisfies Config;

// O tipo inferido é:
// {
//   apiUrl: string;
//   timeout: number;
//   retries: number;
// }
// NÃO é simplesmente "Config"

Se você adicionar uma propriedade que não existe no tipo Config, o compilador rejeita. Valor do tipo errado? Rejeitado. Mas se tudo estiver correto, você mantém acesso preciso às keys específicas do objeto e aos tipos exatos dos valores.

Isso é particularmente valioso em sistemas de temas onde diferentes keys podem ter tipos diferentes:

type Theme = Record<string, string | number[]>;

const palette = {
  red: [255, 0, 0],
  blue: "#0000ff",
  green: [0, 255, 0]
} satisfies Theme;

// Autocomplete sabe que as keys são "red" | "blue" | "green"
// O compilador sabe que red é number[], blue é string
palette.red.map(x => x * 0.8) // funciona
palette.blue.toUpperCase()     // funciona

Esse exemplo vem direto da documentação oficial do TypeScript e ilustra o caso de uso principal: objetos heterogêneos onde diferentes propriedades têm tipos diferentes dentro de uma estrutura geral.

Pattern Combinado: as const satisfies

Uma descoberta importante na prática é que as const e satisfies podem ser combinados: expression as const satisfies Type. Esse pattern oferece o melhor dos dois mundos quando você realmente precisa de imutabilidade profunda com validação estrutural.

const HTTP_METHODS = {
  GET: "GET",
  POST: "POST",
  PUT: "PUT"
} as const satisfies Record<string, string>;

Aqui, as const garante que os valores são literais específicos e o objeto é readonly. O satisfies valida que a estrutura geral está correta (é um Record<string, string>). O resultado é um objeto imutável com tipos literais precisos que passou por validação estrutural completa.

Esse pattern é especialmente útil para enums-like objects, lookup tables e qualquer constante onde mutação seria um erro lógico. A ordem importa: as const vem primeiro para criar os literais, satisfies valida depois.

Vale destacar que isso não adiciona overhead de runtime. O TypeScript remove completamente tanto as const quanto satisfies durante a transpilação — são puramente compile-time checks.

Limitações e Casos Edge

O operador satisfies não funciona bem com generic type parameters sem bound explícito, o que limita seu uso em funções altamente genéricas. A documentação também não especifica como satisfies lida com circular references, e a Issue #47920 no GitHub documenta comportamentos específicos com union types discriminadas que requerem atenção.

Outra limitação importante: satisfies valida apenas estrutura de tipos estáticos. Valores computados ou dinâmicos não são validáveis porque o sistema de tipos do TypeScript opera em compile-time. Se você está construindo configurações a partir de transformações complexas em runtime, satisfies vai validar a estrutura final mas não pode raciocinar sobre a lógica que a gerou.

A ferramenta também não substitui runtime validation. Se você está recebendo dados de APIs externas ou input de usuário, ainda precisa de libraries como Zod ou io-ts. O satisfies garante que seu código TypeScript internamente é type-safe, mas não protege contra dados malformados vindos de fora.

Quando Usar Cada Abordagem

Use type annotations (:) quando você precisa de compatibilidade estrutural e não se importa com perder inferência precisa. Comum em funções que aceitam qualquer objeto que implemente uma interface, onde a flexibilidade importa mais que precisão.

Use as const quando você tem objetos completamente imutáveis e precisa de literais estritos. Enums-like objects e lookup tables são candidatos naturais.

Use satisfies quando você precisa validar a estrutura mas quer manter inferência precisa para autocomplete e type narrowing. Configurações complexas, routing tables e theme systems são os casos de uso principais.

Use as const satisfies quando você precisa de imutabilidade profunda com validação estrutural. É o mais restritivo, mas também o mais seguro para constantes verdadeiramente imutáveis.

A escolha certa depende do seu contexto específico. Migrar entre essas abordagens é relativamente simples — são apenas mudanças sintáticas que o compilador vai ajudar você a validar. O importante é entender o que cada ferramenta faz com a inferência de tipos e escolher conscientemente baseado nas suas necessidades reais.


← Voltar para home