12k
All articles

Um Guia Prático para o Operador `satisfies` do TypeScript

Explicação do operador satisfies do TypeScript com exemplos de config, inferência estreita e quando usá-lo em vez de as ou anotação de tipo.

OpenReplay Team
OpenReplay Team
Um Guia Prático para o Operador `satisfies` do TypeScript

O operador satisfies verifica um valor em relação a um tipo sem alterar o tipo inferido do valor — assim, você obtém a segurança do tipo e a especificidade do valor ao mesmo tempo. Esse comportamento único resolve um problema específico e cotidiano: uma anotação com dois-pontos em um objeto de configuração protege contra valores incorretos, mas descarta as chaves literais e os tipos mais específicos que você queria preservar. Este guia apresenta o modelo mental, o exemplo canônico e uma regra de decisão para escolher satisfies em vez de as ou uma anotação simples com dois-pontos. Todos os exemplos foram escritos para TypeScript 4.9 e versões superiores.

Principais Conclusões

  • satisfies valida um valor em relação a um tipo enquanto preserva o tipo inferido mais específico do valor, garantindo que o autocomplete e o estreitamento de literais continuem funcionando.
  • Com uma anotação de dois-pontos, o tipo declarado prevalece e o valor é ampliado para se adequar a ele; com satisfies, o valor prevalece e o tipo é usado apenas para validação.
  • as não verifica seu valor — ele substitui o verificador de tipos, razão pela qual const user = {} as User compila sem erros e lança uma exceção em tempo de execução ao acessar user.name.
  • Usar anotação e satisfies simultaneamente (const x: T = {…} satisfies T) é redundante: os dois-pontos têm precedência e anulam o estreitamento que você queria.
  • satisfies existe apenas em tempo de compilação e não gera nenhum JavaScript; utilize um validador em tempo de execução como o Zod quando os dados chegarem pela rede ou de um arquivo.

O problema que satisfies resolve

Dois padrões levam os desenvolvedores a usar satisfies. O primeiro é uma anotação com dois-pontos em um objeto com chaves definidas, que amplia o tipo e destrói o autocomplete. O exemplo de routes de Matt Pocock ilustra isso: ao anotar com Record<string, {}>, qualquer chave pode ser lida — até uma inválida — sem gerar erro.

const routes: Record<string, {}> = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};
routes.awdkjanwdkjn; // Nenhum erro — o tipo agora é Record<string, {}>

O segundo padrão envolve uma propriedade com tipo de união. No exemplo do freeCodeCamp, uma propriedade tipada como uma união de um literal de string e um objeto não permite chamar métodos de string sem uma verificação manual.

type Info = "John" | "Jack" | { id: number; age: number };
type Person = { myInfo: Info; myOtherInfo: Info };

const applicant: Person = { myInfo: "John", myOtherInfo: { id: 123, age: 22 } };
applicant.myInfo.toUpperCase();
// Property 'toUpperCase' does not exist on type 'Info'

Você acaba escrevendo if (typeof applicant.myInfo === "string") antes de cada acesso. Ambos os problemas compartilham a mesma causa raiz: a anotação com dois-pontos substituiu o tipo específico do valor por um tipo declarado mais amplo.

O que o operador satisfies faz?

satisfies valida que uma expressão corresponde a um tipo sem alterar o tipo que o TypeScript infere para ela. Foi introduzido no TypeScript 4.9, lançado em 15 de novembro de 2022, e seu comportamento é idêntico nas versões até o atual TypeScript 6.0 estável e o release candidate do 7.0. Como a versão 7.0 em Go manteve a semântica de verificação de tipos estruturalmente idêntica à 6.0, satisfies aplica exatamente as mesmas regras no novo compilador.

O modelo mental, na formulação de Pocock: com uma anotação de dois-pontos, o tipo prevalece sobre o valor; com satisfies, o valor prevalece sobre o tipo. Ao usar satisfies, o TypeScript infere o tipo mais específico possível e usa a anotação apenas para validá-lo. Ele existe somente em tempo de compilação — não gera nenhum JavaScript e não tem custo em tempo de execução, portanto pode detectar um erro de digitação ou um tipo de valor incorreto antes mesmo de o código ser executado.

O exemplo canônico de satisfies

A solução é mover a anotação dos dois-pontos para um satisfies ao final: ambos os problemas desaparecem de uma vez — você mantém os tipos literais mais específicos e ainda recebe um erro para valores incorretos.

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
} satisfies Record<string, {}>;

routes.awdkjanwdkjn;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'

O autocomplete em routes agora lista os caminhos reais. A validação ainda funciona: atribuir algo que a anotação proíbe faz o compilador rejeitar.

const routes = {
  "/": null, // Type 'null' is not assignable to type '{}'
} satisfies Record<string, {}>;

O mesmo recurso resolve o caso da união. applicant.myInfo é estreitado para o literal "John", portanto .toUpperCase() é válido sem nenhuma verificação adicional:

const applicant = {
  myInfo: "John",
  myOtherInfo: { id: 123, age: 22 },
} satisfies Person;

applicant.myInfo.toUpperCase(); // OK — inferido como "John"

satisfies vs as vs anotação com dois-pontos

as não verifica seu valor — ele substitui o verificador de tipos, razão pela qual const user = {} as User compila sem erros e lança uma exceção em tempo de execução no momento em que você acessa user.name. Essa é a distinção central entre as três abordagens:

FerramentaVerifica o valor?Mantém a inferência específica?Permite enganar o TS?Quando usar
: Type (dois-pontos)SimNão — amplia para o tipoNãoQuando você deliberadamente quer um tipo mais amplo
satisfies TypeSimSimNãoQuando você quer validação e inferência específica
as TypeNãoN/ASimQuase nunca como padrão

O risco em tempo de execução é concreto:

type User = { id: string; name: { first: string; last: string } };
const user = {} as User;
user.name.first; // Nenhum erro na IDE — lança exceção em tempo de execução

as também falha silenciosamente. O código abaixo compila hoje, mas ao adicionar um campo obrigatório a User, defaultUser se torna inválido sem nenhum erro:

type User = { id: string; name: string };
const defaultUser = { id: "123", name: "Matt" } as User;

Essa é exatamente a categoria de bug que ferramentas de session replay foram criadas para detectar: você vê o formato real do objeto no momento em que o acesso a uma propriedade lançou a exceção, não o formato que você declarou. Substitua por satisfies e o compilador sinaliza o campo ausente imediatamente.

A regra prática: nunca recorra a as por padrão — use satisfies para validar enquanto mantém a inferência, e use anotação com dois-pontos apenas quando você deliberadamente quiser um tipo mais amplo para reatribuição posterior.

A armadilha da anotação redundante

Usar anotação e satisfies ao mesmo tempo — const joe: TUser = {…} satisfies TUser — é redundante: a anotação com dois-pontos tem precedência e silenciosamente anula o estreitamento que satisfies deveria preservar. O artigo do Refine.dev mostra as consequências — acessar uma propriedade aninhada falha porque o tipo declarado prevaleceu e o estreitamento interno foi descartado. Escolha um ou outro. Se você quer estreitamento, remova os dois-pontos.

Onde satisfies se destaca

Use satisfies para configurações tipadas, mapas com chaves do tipo Record e valores de uniões discriminadas que você quer manter estreitados. Mapas de temas e paletas de cores são o caso de uso arquetípico — o exemplo oficial de palette valida cada entrada RGB enquanto mantém os tipos literais por chave. Para chaves opcionais, envolva o record em Partial para que chaves ausentes sejam permitidas, mas as presentes ainda sejam verificadas:

type Keys = "id" | "name" | "email" | "age";

const person = {
  id: 12345,
  name: "Jacky",
  email: "jacky@test.com",
} satisfies Partial<Record<Keys, string | number>>;

person.name.toUpperCase(); // estreitado para string

Quando não usar satisfies

Dispense o satisfies para um objeto simples onde uma anotação : Type já expressa tudo o que você precisa. Dispense-o também quando você quiser o tipo mais amplo — se você planeja reatribuir uma variável posteriormente, satisfies bloqueará isso porque fixa o tipo inferido mais específico:

// anotação com dois-pontos — reatribuição é permitida
let id: string | number = "123";
id = 456; // OK

// satisfies — o valor prevalece, portanto é estreitado para string
let id2 = "123" satisfies string | number;
id2 = 456; // Type 'number' is not assignable to type 'string'

E dispense-o completamente para dados que você não controla. satisfies nunca é executado, portanto não consegue validar um payload JSON ou uma submissão de formulário em tempo de execução — recorra a um validador de esquema em tempo de execução como Zod ou io-ts quando os dados cruzarem um limite de rede ou arquivo.

Use satisfies sempre que estiver tipando um literal que também quer manter específico — configurações, mapas de rotas, paletas, valores de uniões. Substitua o as reflexivo por ele, mantenha as anotações com dois-pontos para os casos em que um tipo mais amplo é o objetivo, e seu próximo objeto de configuração terá tanto segurança quanto autocomplete.

Perguntas Frequentes

O operador satisfies funciona em arquivos JavaScript com JSDoc?

Sim. O TypeScript 5.0 adicionou uma tag JSDoc @satisfies que faz exatamente o que o operador satisfies faz para arquivos TypeScript. Em um arquivo JavaScript, você escreve a tag acima de uma declaração — por exemplo, uma anotação @satisfies nomeando um tipo — e o verificador valida o valor em relação a esse tipo enquanto mantém o tipo inferido mais específico. Isso permite que projetos JavaScript tipados com JSDoc obtenham o mesmo benefício de validação com estreitamento sem precisar migrar para arquivos .ts.

satisfies adiciona algum custo em tempo de execução ou gera JavaScript extra?

Não. satisfies é um operador exclusivamente de tempo de compilação, em nível de tipos, que não emite nenhum JavaScript — portanto, não tem custo em tempo de execução nem impacto no tamanho do bundle. A palavra-chave e o tipo após ela são apagados durante a compilação, exatamente como uma anotação com dois-pontos. Como nada é executado, ele também não consegue validar dados em tempo de execução, razão pela qual payloads de rede ou entradas de formulário ainda precisam de um validador de esquema em tempo de execução como o Zod.

Por que meu objeto ainda falha na verificação de tipos quando uso tanto uma anotação com dois-pontos quanto satisfies?

Porque a anotação com dois-pontos sempre tem precedência e a cláusula satisfies se torna sintaxe inerte. Escrever const config: Theme = {…} satisfies Theme significa que o tipo Theme declarado prevalece, o valor é ampliado para Theme e a inferência específica que satisfies deveria preservar é descartada. Acessar uma propriedade literal aninhada então falha como se satisfies não estivesse presente. Remova os dois-pontos e mantenha apenas o satisfies ao final para preservar o estreitamento.

Quando devo usar Zod em vez de satisfies para validar dados?

Use Zod, ou outro validador de esquema em tempo de execução como io-ts, sempre que os dados chegarem em tempo de execução de uma fonte que você não controla, como uma resposta de rede, um arquivo JSON ou uma submissão de formulário. satisfies verifica apenas literais que você escreve no código-fonte em tempo de compilação e não emite código em tempo de execução, portanto não consegue inspecionar dados externos desconhecidos. Use satisfies para configurações tipadas, mapas Record e valores de uniões no seu próprio código; use Zod para fronteiras externas.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.