Satisfies operator: validação sem widening

1. O Problema do Widening em TypeScript

Um dos desafios mais sutis no TypeScript é o fenômeno chamado widening — o alargamento automático de tipos literais para tipos mais genéricos. Quando você declara uma variável com let, o TypeScript infere o tipo mais amplo possível:

let status = "ativo"; // tipo inferido: string, não "ativo"
status = "inativo"; // OK
status = "desligado"; // OK (qualquer string é aceita)

Com const, o comportamento muda — o tipo literal é preservado:

const status = "ativo"; // tipo: "ativo" (literal)
// status = "inativo"; // Erro! Não pode reatribuir const

O problema surge em contextos mais complexos, como objetos:

const config = {
  mode: "dark",    // tipo inferido: string (widening!)
  timeout: 3000    // tipo inferido: number (widening!)
};
// config.mode aceitaria qualquer string
// config.timeout aceitaria qualquer número

Anotações de tipo tradicionais resolvem o widening, mas criam outro problema:

const config: { mode: "dark" | "light"; timeout: number } = {
  mode: "dark",
  timeout: 3000
};
// Agora config.mode é "dark" | "light", mas perdemos o literal exato "dark"

Precisávamos de uma forma de validar que um valor está em conformidade com um tipo, sem perder os tipos literais inferidos.

2. Introdução ao Operador satisfies

O operador satisfies, introduzido no TypeScript 4.9, resolve exatamente esse problema. Sua sintaxe é simples:

const valor = expressão satisfies Tipo;

Ele verifica se a expressão à esquerda é compatível com o tipo à direita, sem alterar o tipo inferido da expressão.

const status = "ativo" satisfies string; // tipo: "ativo"
const timeout = 3000 satisfies number;   // tipo: 3000

Compare com as alternativas:

// Type assertion (perigoso):
const a = "hello" as string; // tipo: string (perdeu literal)

// Anotação explícita:
const b: string = "hello";   // tipo: string (perdeu literal)

// satisfies:
const c = "hello" satisfies string; // tipo: "hello" (preservado!)

3. Preservação de Tipos Literais com satisfies

O poder do satisfies brilha quando combinado com tipos literais e uniões:

type Status = "ativo" | "inativo" | "pendente";

const userStatus = "ativo" satisfies Status;
// typeof userStatus = "ativo" (literal preservado)
// userStatus só pode ser "ativo", "inativo" ou "pendente"

// Isso falharia em tempo de compilação:
// const invalidStatus = "cancelado" satisfies Status;
// Erro: '"cancelado"' não é atribuível a 'Status'

Em objetos, a diferença é ainda mais notável:

type Theme = "dark" | "light";
type Config = {
  theme: Theme;
  timeout: number;
  debug: boolean;
};

const appConfig = {
  theme: "dark",    // tipo: "dark" (não Theme)
  timeout: 5000,    // tipo: 5000 (não number)
  debug: true       // tipo: true (não boolean)
} satisfies Config;

// Acessando propriedades com tipos precisos:
appConfig.theme; // tipo: "dark"
appConfig.timeout; // tipo: 5000
appConfig.debug; // tipo: true

// Mas ainda temos validação contra Config:
// appConfig.theme = "blue"; // Erro! "blue" não é Theme

4. Uso com Objetos e Tipos Complexos

O satisfies é particularmente útil para validar objetos complexos sem perder detalhes:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = {
  method: HttpMethod;
  path: string;
  handler: string;
};

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

// Tipo preservado: { users: { method: "GET", ... }, createUser: { method: "POST", ... } }
routes.users.method; // tipo: "GET"
routes.createUser.method; // tipo: "POST"

Com uniões discriminadas, o satisfies permite validação rigorosa:

type SuccessResponse = { status: "success"; data: unknown };
type ErrorResponse = { status: "error"; message: string };
type ApiResponse = SuccessResponse | ErrorResponse;

const response1 = {
  status: "success",
  data: { id: 1, name: "Alice" }
} satisfies ApiResponse;

const response2 = {
  status: "error",
  message: "Not found"
} satisfies ApiResponse;

// response1.data é unknown (preciso), response2.message é string

5. Validação de Mapeamentos e Dicionários

Para dicionários onde queremos garantir que todos os valores seguem um padrão:

type ColorHex = `#${string}`;

const themeColors = {
  primary: "#3498db",
  secondary: "#2ecc71",
  danger: "#e74c3c",
  background: "#ffffff"
} satisfies Record<string, ColorHex>;

// Todos os valores são validados como hexadecimais
// Mas as chaves e tipos literais são preservados

// Isso falharia:
// const badColors = {
//   primary: "blue" // Erro! "blue" não é `#${string}`
// } satisfies Record<string, ColorHex>;

Para mapeamentos mais específicos:

type Permission = "read" | "write" | "admin";
type RolePermissions = Record<string, Permission[]>;

const roles = {
  viewer: ["read"],
  editor: ["read", "write"],
  owner: ["read", "write", "admin"]
} satisfies RolePermissions;

// roles.viewer é do tipo ["read"] (tupla literal)
// roles.owner é ["read", "write", "admin"]

6. Combinação com typeof e Inferência Avançada

O satisfies funciona perfeitamente com typeof para extrair tipos precisos:

const user = {
  name: "Alice",
  age: 30,
  roles: ["admin", "editor"]
} satisfies {
  name: string;
  age: number;
  roles: string[];
};

type UserType = typeof user;
// { name: "Alice"; age: 30; roles: ["admin", "editor"] }

function processUser(u: typeof user) {
  console.log(u.name); // tipo: "Alice"
}

Em funções, para validar retornos complexos:

type ValidationResult = {
  valid: boolean;
  errors?: string[];
};

function validateUser(data: unknown) {
  return {
    valid: true,
    errors: [],
    user: { id: 1, name: "Bob" }
  } satisfies ValidationResult & { user: { id: number; name: string } };
}

const result = validateUser({});
result.valid; // tipo: true
result.user.name; // tipo: "Bob"

Com genéricos:

function createTypedObject<T extends Record<string, unknown>>(
  obj: T satisfies Record<string, string | number>
) {
  return obj;
}

const obj = createTypedObject({
  name: "Alice",
  age: 30
});
// obj.name: string, obj.age: number

7. Diferenças e Boas Práticas em Relação a Alternativas

satisfies vs as (type assertion)

Aspecto satisfies as
Segurança Valida em tempo de compilação Afirmação cega
Preservação de tipos Mantém literais Perde literais
Erro em invalidação Sim, em tempo de compilação Não (pode causar bugs)
// Perigoso:
const x = { value: "hello" } as { value: string | number };
x.value = 42; // OK, mas pode quebrar código que espera string

// Seguro:
const y = { value: "hello" } satisfies { value: string | number };
// y.value = 42; // Erro! "hello" é string, não pode receber number

satisfies vs anotação de tipo direta

// Anotação direta: perde detalhes
const a: { key: string } = { key: "value" };
// a.key é string (perdeu "value")

// satisfies: preserva
const b = { key: "value" } satisfies { key: string };
// b.key é "value" (literal preservado)

Armadilhas comuns

  1. satisfies não valida em runtime — é apenas uma verificação de tipo em tempo de compilação.

  2. Não use satisfies para tipos que você quer que sejam o tipo final — use anotação direta nesse caso.

  3. Cuidado com objetos muito grandessatisfies verifica cada propriedade, o que pode impactar performance em objetos enormes.

  4. Não substitui validação de dados externos — para dados de API ou input de usuário, continue usando bibliotecas de validação em runtime.

Padrões recomendados

  • Use satisfies quando precisar validar contra um tipo e preservar tipos literais
  • Prefira satisfies a as para validação de objetos complexos
  • Combine satisfies com typeof para extrair tipos precisos
  • Use satisfies em definições de configuração e constantes

O satisfies operator é uma ferramenta elegante que preenche uma lacuna importante no sistema de tipos do TypeScript, permitindo validação sem widening e preservando a riqueza dos tipos literais inferidos.

Referências