Verificando tipos em tempo de compilação com satisfies

1. O problema que satisfies resolve

1.1. Inferência vs. validação de tipos: quando o tipo ampliado perde informação

TypeScript é excelente em inferir tipos, mas essa inferência muitas vezes entra em conflito com a necessidade de validar estruturas. Quando anotamos um tipo explicitamente, perdemos informações literais preciosas. O satisfies operador, introduzido no TypeScript 4.9, resolve exatamente esse dilema: ele verifica se um valor satisfaz um tipo sem alterar o tipo inferido.

1.2. O dilema entre as const e anotações explícitas de tipo

Desenvolvedores frequentemente usam as const para preservar literais, mas isso congela tudo. Anotações explícitas ampliam tipos. O satisfies oferece um meio-termo elegante.

1.3. Exemplo motivacional: um objeto de configuração que precisa de tipos literais

// Problema: queremos validar a estrutura, mas manter tipos literais
type Config = {
  api: string;
  timeout: number;
  retry: boolean;
};

const config = {
  api: "https://api.exemplo.com",
  timeout: 5000,
  retry: true,
} satisfies Config;
// config.timeout é inferido como 5000 (literal), não number

2. Sintaxe e funcionamento básico do satisfies

2.1. Estrutura: expressão satisfies Tipo

const resultado = expressao satisfies Tipo;

O compilador verifica se expressao é atribuível a Tipo, mas o tipo de resultado permanece sendo o tipo inferido de expressao.

2.2. Diferença fundamental entre satisfies e anotação de tipo (: Tipo)

  • Anotação (: Tipo): força o valor a ser do tipo Tipo, ampliando ou restringindo.
  • satisfies: apenas verifica compatibilidade, mantendo o tipo inferido original.

2.3. Primeiro exemplo prático

type CorRGB = { r: number; g: number; b: number };

const cor1: CorRGB = { r: 255, g: 0, b: 0 };
// cor1.r é number (perdeu o literal 255)

const cor2 = { r: 255, g: 0, b: 0 } satisfies CorRGB;
// cor2.r é 255 (literal preservado)

3. satisfies vs. anotação de tipo (: Tipo)

3.1. Anotação de tipo amplia o tipo inferido

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

const status1: Status = "ativo";
// status1 é Status, perdemos o literal "ativo"

3.2. satisfies preserva o tipo mais específico

const status2 = "ativo" satisfies Status;
// status2 é "ativo" (literal), mas o compilador verifica que é válido para Status

3.3. Comparação visual com objetos, arrays e tuplas

type Ponto = [number, number, string?];

const ponto1: Ponto = [10, 20, "origem"];
// ponto1[0] é number

const ponto2 = [10, 20, "origem"] as const satisfies Ponto;
// ponto2[0] é 10 (literal), ponto2[2] é "origem" (literal)

type ListaNumeros = number[];
const lista1 = [1, 2, 3] satisfies ListaNumeros;
// lista1 é number[], mas elementos são literais 1, 2, 3

4. satisfies vs. as const + anotação

4.1. as const congela tudo — nem sempre desejável

const frozen = { nome: "João", idade: 30 } as const;
// frozen é { readonly nome: "João"; readonly idade: 30 }
// Não podemos modificar, mesmo que quiséssemos

4.2. Combinando as const com satisfies para máxima precisão

type Pessoa = {
  nome: string;
  idade: number;
  ativo?: boolean;
};

const pessoa = {
  nome: "Maria",
  idade: 28,
  ativo: true,
} as const satisfies Pessoa;
// pessoa é { readonly nome: "Maria"; readonly idade: 28; readonly ativo: true }
// Verifica que satisfaz Pessoa, mas mantém literais e readonly

4.3. Caso de uso: mapeamento de chaves para valores com tipos literais

type CoresValidas = "vermelho" | "azul" | "verde";
type MapaCores = Record<string, CoresValidas>;

const mapa = {
  fundo: "azul",
  texto: "vermelho",
  borda: "verde",
} as const satisfies MapaCores;
// mapa.fundo é "azul" (literal), não string

5. Casos de uso avançados

5.1. Validação de objetos com tipos de união e discriminated unions

type Evento =
  | { tipo: "clique"; x: number; y: number }
  | { tipo: "tecla"; tecla: string };

const evento = {
  tipo: "clique",
  x: 100,
  y: 200,
} satisfies Evento;
// evento.tipo é "clique" (literal), e sabemos que x e y existem

5.2. Garantir que um objeto implemente uma interface, mas manter tipos de propriedades

interface Animal {
  nome: string;
  emitirSom(): string;
}

const gato = {
  nome: "Mimi",
  emitirSom: () => "Miau",
  idade: 3,
} satisfies Animal;
// gato.idade é 3 (literal), e gato satisfaz Animal

5.3. Uso com tipos genéricos e mapeamento de tipos

type Opcoes<T extends string> = Record<T, boolean>;

const opcoes = {
  darkMode: true,
  notificacoes: false,
  som: true,
} satisfies Opcoes<"darkMode" | "notificacoes" | "som">;
// opcoes.darkMode é true (literal)

6. satisfies em funções e parâmetros

6.1. Verificar que um retorno de função satisfaz um tipo sem perder inferência

type Resultado<T> = { dados: T; erro: null } | { dados: null; erro: string };

function buscarUsuario(id: number) {
  return {
    dados: { id, nome: "João" },
    erro: null,
  } satisfies Resultado<{ id: number; nome: string }>;
}

const resultado = buscarUsuario(1);
// resultado.dados.nome é "João" (literal), e sabemos que erro é null

6.2. Validar parâmetros de callback com tipos complexos

type Transformador<T> = (valor: T) => T;

function aplicarTransformacao<T>(valor: T, fn: Transformador<T>): T {
  return fn(valor);
}

const resultado = aplicarTransformacao(5, (x) => x * 2 satisfies number);
// resultado é 10 (literal), mas verificamos que o callback retorna number

6.3. Exemplo: funções que retornam objetos de configuração tipados

type ConfiguracaoServidor = {
  porta: number;
  host: string;
  ssl: boolean;
};

function criarConfiguracao(ambiente: "dev" | "prod") {
  if (ambiente === "dev") {
    return {
      porta: 3000,
      host: "localhost",
      ssl: false,
    } satisfies ConfiguracaoServidor;
  }
  return {
    porta: 443,
    host: "api.exemplo.com",
    ssl: true,
  } satisfies ConfiguracaoServidor;
}

7. Limitações e cuidados ao usar satisfies

7.1. satisfies não é validação em runtime — apenas tempo de compilação

const valor: unknown = JSON.parse('{"nome": "João"}');
// Isso NÃO funciona em runtime:
// const pessoa = valor satisfies { nome: string };
// TypeError: valor.satisfies is not a function

7.2. Não substitui as para type assertions quando necessário

const elemento = document.getElementById("app") satisfies HTMLElement;
// Erro: pode ser null
// Use as para afirmar que não é null:
const elemento2 = document.getElementById("app") as HTMLElement;

7.3. Problemas com tipos condicionais e distribuição em uniões

type IsString<T> = T extends string ? true : false;

const teste = "hello" satisfies IsString<typeof "hello">;
// Isso funciona, mas tipos condicionais complexos podem causar erros inesperados

8. Boas práticas e padrões recomendados

8.1. Quando preferir satisfies em vez de anotação ou as

  • Use satisfies quando precisar de validação de tipo sem perder inferência estreita
  • Prefira anotação (: Tipo) quando quiser ampliar deliberadamente o tipo
  • Use as apenas para type assertions quando você sabe mais que o compilador

8.2. Combinando satisfies com zod ou envalid para validação dupla

import { z } from "zod";

const SchemaUsuario = z.object({
  nome: z.string(),
  idade: z.number().positive(),
});

type Usuario = z.infer<typeof SchemaUsuario>;

const usuario = {
  nome: "Ana",
  idade: 25,
} satisfies Usuario;

// Validação em runtime:
SchemaUsuario.parse(usuario);

8.3. Refatorando código existente para usar satisfies com segurança

// Antes: perdendo tipos literais
type Cores = "red" | "green" | "blue";
const cor: Cores = "red"; // cor é Cores

// Depois: mantendo tipos literais
const cor = "red" satisfies Cores; // cor é "red"

// Refatoração segura:
// 1. Substitua `: Tipo` por `satisfies Tipo`
// 2. Verifique se o código downstream não depende do tipo ampliado
// 3. Ajuste funções que esperam tipos literais

O satisfies é uma ferramenta poderosa para escrever TypeScript mais expressivo e preciso. Ele permite validar estruturas complexas sem sacrificar a riqueza dos tipos inferidos, resultando em código mais seguro e auto-documentado.

Referências