← TypeScript: tipos, contratos e diagnóstico
08 / 10 · 60 MIN

Dados externos e fronteiras de erro

Transforma dados desconhecidos em comandos delimitados, distinguindo sintaxe, contrato e falhas da operação que usa os dados.

Parsing é apenas a primeira fronteira

JSON.parse pode terminar com sucesso e produzir null, uma lista, um número ou um objeto que não cumpre o modelo de negócio. Uma assertion as Command não inspeciona esses valores. Guardar o resultado como unknown impede que o restante código use diretamente campos em que ainda não há razão para confiar.

A revisão começa pelo contrato observável: que formas são aceites, quais são recusadas e quando podem começar os efeitos? No exemplo, uma operação retry precisa de um id com texto útil e de um limit numérico delimitado. A string "3" não é aceite como número por conveniência. Essa decisão evita esconder uma diferença de contrato através de coerção silenciosa.

Existência, tipo e regra de domínio

Para aplicar in a um unknown, começa por estabelecer um objeto não nulo. typeof value === "object" não chega, porque null também produz esse resultado. Se o domínio exige um registo em vez de um array, essa condição é outra verificação. Depois, a existência de id permite consultar o campo, mas ainda não demonstra que seja string.

Cada regra acrescenta uma garantia distinta. Um número pode ser fracionário, NaN ou estar fora do intervalo permitido; typeof não verifica essas propriedades. No exemplo, Number.isInteger e os limites inclusivos completam a regra de limit. Para id, a verificação rejeita texto sem caracteres úteis mas preserva o valor original, sem alterar a identidade através de normalização implícita.

A promessa de um predicado deve ser demonstrada

Uma função isAccount(x): x is Account comunica ao chamador que, no ramo verdadeiro, x pode ser usado como Account. O compilador usa essa promessa, mas não gera as verificações necessárias a partir da interface. Um corpo que devolve sempre true pode criar confiança indevida mesmo quando a assinatura passa a verificação.

Revê todos os caminhos de retorno verdadeiro e compara-os com os campos obrigatórios, as variantes e as regras do domínio. Considera também o resultado falso: um predicado que só aceita uma parte legítima do tipo pode ensinar ao chamador uma exclusão incorreta. Quando a regra é uma validação de negócio mais estreita, um resultado explícito com dados projetados pode ser mais claro do que um predicado geral.

Assertions que interrompem o fluxo

Uma função assertText(x): asserts x is string promete que um retorno normal permite continuar com x textual. O corpo precisa de impedir esse retorno quando a promessa é falsa, por exemplo lançando TypeError. Um corpo vazio não estabelece a garantia. Devolver false é o protocolo de um predicado booleano, não o desta assinatura de assertion.

A regra deve ter o alcance declarado. Se o contrato exige apenas string, uma string vazia é válida; se exige texto não vazio, é necessária outra verificação e uma descrição coerente. Da mesma forma, anotar o retorno de um coordenador como Result não impede que uma assertion chamada por ele lance. O coordenador tem de tratar essa interrupção se prometeu representá-la num ramo de resultado.

Falhas também atravessam fronteiras de tipos

JavaScript permite lançar valores que não sejam Error. Com useUnknownInCatchVariables, o catch obriga a verificar o valor antes de ler propriedades. instanceof Error permite tratar uma instância reconhecida nesse contexto, mas ainda é preciso decidir o que fazer com os restantes valores. Aplicar as Error não cria message numa string lançada.

O estado da falha deve sobreviver ao tratamento. Devolver sucesso com uma lista vazia após qualquer exceção torna impossível distinguir uma consulta vazia de uma consulta que não terminou. Um código estável de falha pode ser publicado enquanto detalhes ficam em registo interno controlado. Não é necessário expor o texto arbitrário lançado por uma dependência para comunicar um desfecho útil.

Classificar a etapa que realmente falhou

Um único try que contém parsing, validação e envio pode apanhar falhas com causas muito diferentes. Se o catch devolve sempre invalid-json, uma indisponibilidade da integração passa a ser atribuída ao pedido. O utilizador pode tentar corrigir dados válidos enquanto o problema real continua por tratar.

Delimita as etapas segundo os desfechos que o consumidor precisa de distinguir. O parsing pode produzir invalid-json; a verificação de campos pode produzir invalid-command; a operação posterior pode falhar por outra razão. Esta separação não exige publicar detalhes internos. Exige que cada código seja sustentado pelo âmbito observado e que os efeitos só comecem depois de existir um comando aceite.

Leitura guiada de um comando aceite

O exemplo não executa comandos. O try contém apenas JSON.parse; o restante fluxo verifica forma, id e variante. stop não precisa de limit. retry exige um número inteiro entre 0 e 5. Um kind desconhecido termina no ramo invalid-command. Os retornos aceites constroem objetos novos com os campos previstos, pelo que campos desconhecidos não são propagados.

Essa política deve ser lida exatamente: os campos desconhecidos são ignorados, não fazem o pedido falhar. id só com espaços é recusado, mas id com texto e espaços exteriores é conservado. A entrada é JSON interpretado neste contexto, não um objeto arbitrário com getters ou Proxy. Se a fronteira mudar, essas premissas também têm de ser revistas.

// TypeScript 7.0; strict: true; ES2022 library. Input is JSON text, not arbitrary objects.
type Command =
  | { kind: "retry"; id: string; limit: number }
  | { kind: "stop"; id: string };
type DecodeResult =
  | { ok: true; command: Command }
  | { ok: false; reason: "invalid-json" | "invalid-command" };

function decodeCommand(text: string): DecodeResult {
  let raw: unknown;
  try {
    raw = JSON.parse(text);
  } catch {
    return { ok: false, reason: "invalid-json" };
  }
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
    return { ok: false, reason: "invalid-command" };
  }
  if (!("kind" in raw) || !("id" in raw) ||
      typeof raw.id !== "string" || raw.id.trim().length === 0) {
    return { ok: false, reason: "invalid-command" };
  }
  if (raw.kind === "stop") {
    return { ok: true, command: { kind: "stop", id: raw.id } };
  }
  if (raw.kind === "retry" && "limit" in raw &&
      typeof raw.limit === "number" && Number.isInteger(raw.limit) &&
      raw.limit >= 0 && raw.limit <= 5) {
    return { ok: true, command: { kind: "retry", id: raw.id, limit: raw.limit } };
  }
  return { ok: false, reason: "invalid-command" };
}
NA PRÁTICA

Segue um JSON mal formado, um retry com limit textual, um retry com limit zero e um stop com id válido. Só o parser está no try. Campos desconhecidos são ignorados na projeção; id é preservado e apenas se rejeita texto vazio ou só com espaços.

Armadilhas comuns

Confiar em as ou satisfies sobre any; declarar um predicado sem validar; ler message em qualquer catch; classificar uma falha de envio como JSON inválido.

Tópicos relacionados: Tipos estáticos e valores em execução · Uniões, refinamento de tipos e modelos de estado · Configuração, entrega e migração

Leva esta ideia contigo

Tipos conservam garantias que a fronteira estabelece. Mantém validação e efeitos separados, e comunica a falha da etapa que realmente falhou.

Criar conta

Referência: TypeScript Handbook: Everyday Types · TypeScript 7.0; explicit strict and host configuration