Satisfies e as: quando usar cada operador de tipo

1. Introdução aos operadores de tipo no TypeScript

TypeScript oferece duas ferramentas poderosas para lidar com tipos em situações onde o compilador não consegue inferir ou validar automaticamente: as (type assertion) e satisfies (type satisfaction). Embora ambos lidem com a relação entre valores e tipos, eles resolvem problemas fundamentalmente diferentes.

O operador as é uma afirmação de tipo — você diz ao compilador "confie em mim, eu sei o que estou fazendo". Ele existe desde as primeiras versões do TypeScript e permite forçar um tipo sobre um valor, mesmo que o compilador discorde.

Já o operador satisfies, introduzido no TypeScript 4.9, é uma validação de tipo — você diz ao compilador "verifique se este valor está de acordo com este tipo, mas mantenha o tipo mais específico que você inferir". Ele não altera o tipo inferido, apenas valida a conformidade.

A diferença crucial: as altera o tipo para o que você declarar; satisfies valida sem perder informações.

2. as (Type Assertion): quando e como usar

O as tem cenários legítimos, mas seu uso deve ser controlado. Os principais casos incluem:

  • APIs DOM: quando você sabe que um elemento tem um tipo específico
  • Dados de terceiros: quando você confia na estrutura de dados externos
  • Migração de código legado: como ponte temporário para tipagem gradual
// Cenário legítimo: DOM API
const appElement = document.getElementById('app') as HTMLDivElement;
appElement.innerHTML = 'Hello';

// Cenário arriscado: supressão de erro
const input = document.getElementById('input') as HTMLInputElement;
// Se o elemento não existir, isso lançará erro em runtime

O as const é uma variação especial que transforma literais em tipos literais imutáveis:

const colors = ['red', 'green', 'blue'] as const;
// Tipo: readonly ["red", "green", "blue"]
// Não é string[], mas uma tupla literal

const status = 'active' as const;
// Tipo: "active" (não string)

O risco do as é a supressão de erros legítimos:

const user = JSON.parse('{"name": "Alice"}') as { name: string; age: number };
console.log(user.age); // undefined em runtime, mas TypeScript não reclama

3. satisfies: validação sem perda de informação

O satisfies resolve um problema clássico: validar que um objeto está de acordo com um tipo amplo, sem perder os tipos específicos de suas propriedades.

type Config = Record<string, string | number>;

// Sem satisfies: tipo inferido é Config (perde detalhes)
const config1: Config = {
  port: 3000,
  host: 'localhost'
};
// config1.port é string | number (perdemos que é number)

// Com satisfies: valida mas mantém tipos específicos
const config2 = {
  port: 3000,
  host: 'localhost'
} satisfies Config;
// config2.port é number (tipo original preservado)

O benefício é claro: podemos acessar propriedades com tipos precisos:

type Palette = Record<string, string | { r: number; g: number; b: number }>;

const palette = {
  red: '#ff0000',
  blue: { r: 0, g: 0, b: 255 },
  green: '#00ff00'
} satisfies Palette;

palette.red.toUpperCase(); // OK: string
palette.blue.r; // OK: number (não precisamos de type guard)

4. Comparação direta: as vs satisfies

Aspecto as satisfies
Segurança Baixa (supressão) Alta (validação)
Inferência Perde tipo original Preserva tipo original
Perda de info Sim Não
Uso com any/unknown Necessário Não funciona

Quando as é a única opção:

const data: unknown = JSON.parse('{"id": 1}');
// Precisamos de as para usar o valor
const user = data as { id: number };

Quando satisfies é superior:

type Shape = 
  | { type: 'circle'; radius: number }
  | { type: 'rectangle'; width: number; height: number };

const shape = { type: 'circle', radius: 5 } satisfies Shape;
// shape.type é "circle" (não string)
// shape.radius é number

Exemplo lado a lado:

// Com as: perde informação
const colors1 = {
  primary: '#333',
  secondary: '#666'
} as Record<string, string>;
// colors1.primary é string (ok, mas poderia ser mais específico)

// Com satisfies: mantém literais
const colors2 = {
  primary: '#333',
  secondary: '#666'
} satisfies Record<string, string>;
// colors2.primary é "#333" (string literal)

5. Padrões avançados com satisfies

Combinando satisfies com tipos genéricos e keyof:

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string };
};

const handlers = {
  click: (e: { x: number; y: number }) => console.log(e.x, e.y),
  keydown: (e: { key: string }) => console.log(e.key)
} satisfies { [K in keyof EventMap]: (e: EventMap[K]) => void };

// handlers.click tem tipo (e: { x: number; y: number }) => void
// handlers.keydown tem tipo (e: { key: string }) => void

Uso em funções de alta ordem:

function createValidator<T>(schema: T) {
  return (data: unknown): data is T => {
    // lógica de validação
    return true;
  };
}

const userValidator = createValidator({
  name: 'string',
  age: 'number'
} satisfies Record<string, 'string' | 'number'>);

Limitações: satisfies não funciona com tipos condicionais complexos:

// Isso não funciona como esperado
type IsString<T> = T extends string ? true : false;
const test = "hello" satisfies IsString<typeof "hello">; // Erro

6. Armadilhas comuns e boas práticas

Erro frequente: usar as quando satisfies seria mais seguro:

// ❌ Ruim: perde verificação de propriedades extras
const config = { port: 3000, unknownProp: true } as Record<string, number>;

// ✅ Bom: valida e mantém tipos
const config = { port: 3000 } satisfies Record<string, number>;
// Se adicionar unknownProp, TypeScript reclama

as em objetos literais: perde verificação de propriedades extras:

interface User { name: string; age: number; }

// ❌ Não reclama de propriedade extra
const user1 = { name: 'Alice', age: 30, admin: true } as User;

// ✅ Reclama de admin não estar em User
const user2 = { name: 'Alice', age: 30, admin: true } satisfies User;

satisfies com as const: garantindo imutabilidade e validação:

const ROLES = {
  ADMIN: 'admin',
  USER: 'user'
} as const satisfies Record<string, string>;

// Tipo: { readonly ADMIN: "admin"; readonly USER: "user" }
// Valida que todos os valores são strings

Recomendação geral: prefira satisfies para validação e as apenas para escapes controlados (como any/unknown ou DOM APIs).

7. Conclusão e guia de decisão

A escolha entre as e satisfies segue um fluxo simples:

  1. Você está lidando com any ou unknown? → Use as (é a única opção)
  2. Você precisa validar um objeto contra um tipo amplo? → Use satisfies
  3. Você está acessando APIs DOM? → Use as (ou considere type narrowing)
  4. Você quer criar literais imutáveis? → Use as const
  5. Você quer validar sem perder tipos específicos? → Use satisfies

Regras de ouro:
- Segurança primeiro: satisfies sempre que possível
- as é um escape, não um padrão
- Documente todo uso de as com comentários explicando o porquê
- Prefira type narrowing (type guards, discriminated unions) sobre as

O TypeScript evolui para tornar o satisfies cada vez mais útil. Com a introdução de pattern matching e type narrowing mais avançados em versões futuras, a tendência é que as se torne cada vez mais raro em código bem tipado.

Referências