← TypeScript: tipos, contratos e diagnóstico
04 / 10 · 55 MIN

Objetos e compatibilidade estrutural

Distingue compatibilidade estrutural, propriedades excedentes, ausência e readonly; projeta dados e separa referências quando o contrato exige isolamento.

Compatibilidade compara a estrutura necessária

Para objetos simples com propriedades públicas, a compatibilidade verifica se a origem fornece os membros exigidos pelo destino com tipos compatíveis. Uma variável com id e attempts pode ser atribuída a uma vista que exige apenas id. Os nomes dos tipos não criam, por si só, uma hierarquia nominal nem uma operação de transformação.

A atribuição continua a referir o mesmo objeto. Não apaga attempts, não copia os dados e não escolhe o que será serializado. Esta distinção é essencial em fronteiras públicas: reduzir o tipo visível ao consumidor não reduz automaticamente o payload observado em execução. Uma projeção explícita pode ser necessária mesmo quando a compatibilidade estática passa.

Literais novos recebem uma verificação adicional

Um literal diretamente atribuído a Config é sujeito a verificação de propriedades excedentes. Um campo retires quando o contrato declara retries pode ser denunciado como provável erro de escrita. Esta regra ajuda a encontrar falhas em objetos de configuração, onde uma opção ignorada pode ter consequências difíceis de observar.

Guardar primeiro o literal numa variável pode alterar essa verificação, mas não corrige o nome nem elimina o campo. Uma assertion também pode contornar o diagnóstico sem melhorar o contrato. Se campos adicionais são realmente permitidos, deve existir uma política explícita para eles. Se a intenção era usar uma opção conhecida, corrige a propriedade em vez de esconder o aviso.

Ausência não é sempre undefined explícito

Com exactOptionalPropertyTypes: true e strict: true, theme?: "light" | "dark" admite ausência da propriedade e os valores declarados quando ela está presente. Não acrescenta automaticamente undefined aos valores que podem ser escritos. Se o contrato precisa desse estado presente, tem de o declarar.

A leitura continua a poder obter undefined quando a propriedade está ausente. Por isso, a opção não elimina a necessidade de tratar ausência no consumidor. A diferença pode importar para atualizações parciais: não enviar uma chave e enviar uma chave com valor undefined não são necessariamente a mesma decisão antes da serialização. A política deve ser definida no protocolo e refletida no tipo.

readonly restringe um caminho de acesso

Uma propriedade readonly não pode ser reatribuída através desse tipo durante a verificação. Isso não implica congelamento do objeto em execução. Se outra variável aponta para o mesmo objeto com acesso mutável, uma alteração por essa variável pode ser observada na vista readonly.

O modificador também não é automaticamente profundo. Em readonly policy: { limit: number }, a referência policy não pode ser substituída através dessa vista, mas limit continua mutável. Para um snapshot, é necessário decidir que níveis são copiados e que acessos tipados ficam restritos. Copiar só o objeto exterior deixa os filhos partilhados; congelar a origem pode contrariar a necessidade de o proprietário continuar a trabalhar.

Interseção combina exigências, não substitui campos

O operador & exige que um valor satisfaça simultaneamente os contratos combinados. Se ambos declaram value, um como string e outro como number, o campo teria de cumprir as duas exigências. Isso produz never para o campo em vez de escolher o último tipo ou criar uma união. Não é a mesma operação que construir um objeto com spreads e sobrescrever uma propriedade.

Uma interface que pretende estender outra também tem de manter membros compatíveis. Trocar um id textual por numérico exige uma decisão sobre a fronteira e sobre os consumidores antigos. Um tipo separado e um adaptador explícito podem expressar essa diferença melhor do que uma interseção impossível ou uma assertion que finge substituição segura.

Mapas homogéneos e metadata separada

Uma assinatura [key: string]: number afirma que os valores das propriedades abrangidas são numéricos. Uma propriedade label: string no mesmo objeto contradiz essa exigência. Alargar tudo para number | string remove o conflito, mas também permite texto nos valores que talvez devessem continuar exclusivamente numéricos.

Se o contrato pede metadata textual e contagens por chave, uma estrutura com label separado de counts pode conservar as duas regras. Essa estrutura não prova que uma chave concreta exista: a ausência e as opções de leitura indexada serão tratadas no módulo de coleções. Aqui a decisão é sobre valores presentes e sobre a separação entre o nome do conjunto e os seus dados homogéneos.

Leitura guiada da projeção e do snapshot

capture constrói um objeto novo com os campos conhecidos, um novo policy e um novo array tags. label, limit e os elementos de tags são valores primitivos no esquema apresentado. Assim, alterações posteriores do proprietário nos níveis mutáveis não são partilhadas com este resultado. Se o esquema ganhar objetos dentro de tags, a estratégia precisa de ser revista.

O tipo Snapshot restringe escritas por consumidores tipados, mas não cria uma garantia universal contra código JavaScript ou assertions incorretas. Nos casos, separa duas necessidades: limitar campos enviados e conservar valores de aprovação. A primeira exige projeção; a segunda exige analisar referências. Uma anotação mais estreita não executa nenhuma dessas operações.

// TypeScript 7.0; strict: true; plain data, complete schema below.
type Owner = {
  label: string;
  policy: { limit: number };
  tags: string[];
};
type Snapshot = {
  readonly label: string;
  readonly policy: { readonly limit: number };
  readonly tags: readonly string[];
};
function capture(owner: Owner): Snapshot {
  return {
    label: owner.label,
    policy: { limit: owner.policy.limit },
    tags: [...owner.tags]
  };
}
NA PRÁTICA

Lê as referências criadas por capture. A cópia separa policy e tags do proprietário para este esquema de valores simples. readonly restringe os acessos tipados ao snapshot, mas não executa Object.freeze nem valida dados externos.

Armadilhas comuns

Confundir uma vista tipada com projeção; tratar readonly como congelamento profundo; ignorar a diferença entre ausência e undefined; usar interseção como substituição de propriedades.

Tópicos relacionados: Tipos estáticos e valores em execução · Tipos mapeados e modelos condicionais · Coleções, ausência e mutação

Leva esta ideia contigo

O contrato estático e o objeto real são coisas relacionadas, mas distintas. A forma de publicação e a partilha de referências exigem decisões explícitas.

Criar conta

Referência: TypeScript: Structural compatibility · TypeScript 7.0; explicit strict and host configuration