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

Genéricos e relações entre chaves

Usa genéricos para conservar relações entre entradas e resultados; distingue restrições, chaves conhecidas e validação de dados.

Uma relação útil, não uma etiqueta

Imagina uma função que recebe um registo e devolve esse mesmo registo depois de o colocar numa fila local. Se o retorno for object, o consumidor perde os campos concretos; se for any, perde a verificação. Um parâmetro T usado na entrada e no retorno conserva a relação sem exigir uma versão da função por cada modelo.

T não identifica uma classe em execução nem produz uma cópia. A função tem de funcionar para todos os tipos que a assinatura admite. Antes de acrescentar um genérico, escreve a relação pretendida: o resultado conserva o tipo da entrada, um callback determina o resultado, ou uma chave determina o tipo de um campo. Se não houver relação útil, um parâmetro normal pode ser mais claro.

Restringir a capacidade de que o corpo precisa

Uma função genérica que lê value.id precisa de uma promessa sobre id. A restrição T extends { id: string } fornece essa capacidade, mantendo a possibilidade de T conter outros campos. T extends object é insuficiente: nem todos os objetos têm id. Um valor predefinido T = { id: string } também não substitui uma restrição; outros argumentos de tipo continuam possíveis.

A restrição deve refletir o uso. Se o corpo apenas lê id e devolve o objeto, não precisa de exigir campos específicos de Customer. Isso permite reutilização sem enfraquecer o retorno. A contrapartida é que o corpo só pode contar com as capacidades declaradas, mesmo que a primeira chamada observada contenha muitos mais campos.

O limite de construir um T desconhecido

Um repositório que promete devolver T não pode resolver uma ausência criando apenas { id }. Esse objeto cumpre a restrição mínima, mas um consumidor pode ter escolhido Customer com email obrigatório ou um subtipo com uma condição mais específica. Aplicar as T esconde essa diferença sem fornecer os dados.

Há decisões de desenho legítimas: devolver T | undefined e obrigar o chamador a tratar ausência; devolver um contrato mínimo diferente; ou receber uma função de criação que sabe produzir T. Escolhe segundo a responsabilidade do componente. Uma fábrica recebida não faz o repositório conhecer o domínio: transfere explicitamente a construção para quem o conhece.

Chaves conhecidas e resultado selecionado

Para um registo com id e attempts, keyof produz a união dos nomes das propriedades declaradas. Um segundo parâmetro K extends keyof T relaciona a chave com o objeto. O retorno T[K] conserva o tipo da propriedade escolhida: a chave literal attempts pode produzir number em vez de uma união ampla de todos os valores.

Se a chave já for uma união, o resultado da leitura também pode ser uma união. Isso não cria automaticamente um par discriminado com chave e valor correlacionados. Além disso, uma assinatura de índice string descreve um conjunto aberto de chaves; não é equivalente a uma lista pública finita. Define a lista autorizada na fronteira da aplicação, mesmo que o objeto interno venha a ganhar novos campos.

Tipos derivados de valores conhecidos

Quando um array local já define a forma dos seus elementos, (typeof entries)[number] permite consultar esse tipo sem o escrever outra vez. typeof é usado aqui no espaço de tipos; não é o operador de execução que devolve a string "object". O índice number seleciona os elementos do array, e um segundo índice pode selecionar uma propriedade desses elementos.

Esta derivação reduz duplicação, mas não altera os dados nem resolve uma fronteira externa. Um array obtido de uma fonte sem validação não passa a estar validado por se consultar o seu tipo. Distingue também um nome de valor de um alias: entries[number] não é a forma de obter o tipo quando entries foi declarado apenas como variável.

Inferência e valores predefinidos têm limites

Um alias Packet<T = string> permite escrever Packet para um payload textual; escrever Packet<number> escolhe outro contrato. O valor predefinido é uma conveniência de tipos. Não torna payload opcional, não converte um número em texto e não preenche um objeto vazio. Em funções, a inferência a partir dos argumentos pode fornecer um candidato antes de ser necessário recorrer ao valor predefinido.

O mesmo cuidado aplica-se a decode<Account>(text). Se o corpo apenas interpretar JSON, escolher Account não inspeciona propriedades. Uma fronteira honesta devolve unknown até existir validação, ou recebe uma função que faça essa validação e produza o resultado. O parâmetro genérico conserva uma garantia que a implementação tem de estabelecer.

Leitura guiada de uma chave pública

No exemplo, row já respeita Job. O dado incerto é field: string. A condição rejeita tudo o que não seja id ou attempts; depois desse retorno antecipado, field tem a união finita que readField aceita. A lista continua explícita, pelo que acrescentar um campo interno não o torna automaticamente acessível neste ecrã.

Segue os três caminhos sem executar: id, attempts e um texto desconhecido. Identifica a forma do resultado e quem trata ok: false. O helper genérico lê apenas o campo selecionado; não valida o registo, não copia o objeto e não converte a saída para texto. Se a apresentação exigir sempre string, essa conversão pertence a uma decisão adicional e visível.

// TypeScript 7.0; strict: true. Input is already validated.
function readField<T, K extends keyof T>(row: T, key: K): T[K] {
  return row[key];
}

type Job = { id: string; attempts: number };
type DisplayResult =
  | { ok: true; value: string | number }
  | { ok: false; reason: "unsupported-field" };

function displayField(row: Job, field: string): DisplayResult {
  if (field !== "id" && field !== "attempts") {
    return { ok: false, reason: "unsupported-field" };
  }
  return { ok: true, value: readField(row, field) };
}
NA PRÁTICA

Lê o ramo de rejeição e identifica o tipo de field depois dele. O refinamento admite apenas as duas chaves públicas. No ramo aceite, value pode ser texto ou número; o resultado não guarda qual das duas chaves foi usada.

Armadilhas comuns

Confundir uma restrição mínima com um construtor de T; usar any para esconder a relação; aceitar uma chave externa através de uma assertion.

Tópicos relacionados: Funções e contratos de chamada · Objetos e compatibilidade estrutural · Tipos mapeados e modelos condicionais

Leva esta ideia contigo

Cada parâmetro de tipo deve conservar uma relação útil. O corpo continua responsável por produzir valores válidos e por validar as fronteiras reais.

Criar conta

Referência: TypeScript: working with generic type variables · TypeScript 7.0; explicit strict and host configuration