Tips & Tricks (Atualizado: 02/06/2026)

Generics de TypeScript com Claude Code: keyof, constraints e tipos de API

Aprenda TypeScript Generics com Claude Code usando constraints, keyof, mapped types, tipos de API e verificação com tsc.

Generics de TypeScript com Claude Code: keyof, constraints e tipos de API

Por que um prompt genérico demais enfraquece o tipo

TypeScript Generics permite que uma função, um tipo ou uma classe funcione com várias formas de dados sem perder a relação entre entrada e saída. A dificuldade para iniciantes não é a letra T. O problema é pedir ao Claude Code “deixe isso genérico” sem explicar quais partes devem continuar flexíveis e quais partes precisam ser limitadas. O resultado costuma parecer reutilizável, mas pode depender de any, unknown ou de um Record<string, unknown> amplo demais.

Em projetos reais, generics ficam perto de respostas de API, formulários, contas, billing, eventos de analytics e CTAs de produto. Se o tipo for amplo nesse ponto, o erro não afeta só o autocomplete: um formulário de lead, checkout ou evento de conversão pode aceitar dados inválidos. A regra prática de Masa é pedir sempre um exemplo válido e um exemplo que deve falhar no compilador. Sem essa prova com tsc, o tipo ainda é só uma promessa.

O fluxo mental deste artigo é:

valor de entrada -> capturado como T -> chave limitada por keyof T -> estrutura transformada com mapped types -> contrato validado com tsc

A sintaxe foi conferida na documentação oficial do TypeScript: Generics, operador keyof, Mapped Types e Conditional Types. Para ampliar o workflow com Claude Code, leia também dicas de TypeScript com Claude Code e utility types com Claude Code.

A revisão vem antes da implementação

Generics são ferramentas de compilação. T não é uma variável de runtime; é um parâmetro de tipo que ajuda o compilador a lembrar qual tipo entrou e qual tipo deve sair. Neste contexto, extends significa “aceite apenas tipos que satisfaçam esta forma”. keyof T cria o conjunto de nomes de propriedades de T. Um mapped type percorre esses nomes e constrói um novo tipo.

Antes de deixar Claude Code alterar o repositório, entregue um contrato como este:

PerguntaO que dizer ao Claude CodeO que revisar
O que T representa?Objeto de domínio, DTO ou modelo de formulárioO retorno não perde o tipo original
O que deve ser limitado?K extends keyof T, E extends ApiError, T extends objectChamadas inválidas falham ao compilar
Como verificar?@ts-expect-error, Expect, comando estrito de tscO exemplo ruim realmente falha

Isso evita um erro comum: Claude Code cria algo que compila apenas porque colocou um cast no final. Um cast pode ser aceitável quando documenta uma transformação que TypeScript não consegue inferir, mas não deve esconder um design amplo demais.

Caso 1: remover duplicados com uma chave segura

O primeiro caso é uniqueBy, útil para linhas de API, imports CSV, tabelas administrativas e listas de interface. Se key for apenas string, a chamada aceita uma propriedade que não existe. Com K extends keyof T, a chave precisa ser uma propriedade real do item.

type User = {
  id: string;
  email: string;
  role: "admin" | "editor";
  score: number;
};

function uniqueBy<T>(items: readonly T[]): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key: K): T[];
function uniqueBy<T, K extends keyof T>(items: readonly T[], key?: K): T[] {
  const seen = new Set<unknown>();
  const output: T[] = [];

  for (const item of items) {
    const value = key === undefined ? item : item[key];
    if (seen.has(value)) continue;
    seen.add(value);
    output.push(item);
  }

  return output;
}

const users: User[] = [
  { id: "u_1", email: "masa@example.com", role: "admin", score: 92 },
  { id: "u_2", email: "editor@example.com", role: "editor", score: 88 },
  { id: "u_1", email: "masa+copy@example.com", role: "admin", score: 70 },
];

const byId = uniqueBy(users, "id");
const byRole = uniqueBy(users, "role");

// @ts-expect-error "missing" is not a key of User.
uniqueBy(users, "missing");

console.log(byId.map((user) => user.id));
console.log(byRole.map((user) => user.role));

O prompt deve ser direto: “use overloads, limite key a keyof T e inclua um @ts-expect-error para uma chave inexistente”. Sem isso, Claude Code pode escrever key: string junto com item[key as keyof T], o que adia o erro para runtime.

Caso 2: resposta de API sem optional confuso

O segundo caso é um tipo de resposta de API. Muitos projetos usam data?: T e error?: ApiError na mesma interface. Parece prático, mas cada caller precisa verificar se data existe, se error existe ou se o estado ficou impossível. Uma união discriminada deixa claro: sucesso tem data, falha tem error, e ok estreita o tipo.

type ApiError = {
  code: string;
  message: string;
  retryable: boolean;
};

type ApiResult<T, E extends ApiError = ApiError> =
  | { ok: true; status: number; data: T; error?: never }
  | { ok: false; status: number; error: E; data?: never };

type UserDto = {
  id: string;
  name: string;
  plan: "free" | "pro";
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

function parseUserResponse(json: unknown): ApiResult<UserDto> {
  if (
    isRecord(json) &&
    typeof json.id === "string" &&
    typeof json.name === "string" &&
    (json.plan === "free" || json.plan === "pro")
  ) {
    return {
      ok: true,
      status: 200,
      data: { id: json.id, name: json.name, plan: json.plan },
    };
  }

  return {
    ok: false,
    status: 422,
    error: {
      code: "INVALID_USER_RESPONSE",
      message: "User response does not match the expected shape.",
      retryable: false,
    },
  };
}

function unwrap<T, E extends ApiError>(result: ApiResult<T, E>): T {
  if (result.ok) {
    return result.data;
  }

  throw new Error(`${result.error.code}: ${result.error.message}`);
}

const parsed = parseUserResponse({ id: "u_1", name: "Masa", plan: "pro" });
const user = unwrap(parsed);
console.log(user.name.toUpperCase());

Esse padrão combina bem com Claude Code porque o prompt descreve a validação de runtime e o contrato de tipo no mesmo lugar. Para contexto backend, veja desenvolvimento de API com Claude Code e testes de API com Claude Code.

Caso 3: estado de formulário com mapped types

O terceiro caso é o estado de um formulário. Começamos com um modelo de negócio e derivamos, para cada campo, um estado com value, dirty e errors. Mapped types evitam duplicar nomes de campos e preservam o tipo do valor: email continua string, seats continua number, newsletter continua boolean.

type FieldState<T> = {
  value: T;
  dirty: boolean;
  errors: string[];
};

type FormState<T extends object> = {
  [K in keyof T]: FieldState<T[K]>;
};

function createFormState<T extends object>(initial: T): FormState<T> {
  const entries = Object.entries(initial).map(([key, value]) => [
    key,
    { value, dirty: false, errors: [] },
  ]);

  return Object.fromEntries(entries) as FormState<T>;
}

function setField<T extends object, K extends keyof T>(
  state: FormState<T>,
  key: K,
  value: T[K],
): FormState<T> {
  return {
    ...state,
    [key]: { value, dirty: true, errors: [] },
  } as FormState<T>;
}

type SignupForm = {
  email: string;
  seats: number;
  newsletter: boolean;
};

const form = createFormState<SignupForm>({
  email: "team@example.com",
  seats: 2,
  newsletter: true,
});

const updated = setField(form, "seats", 3);

// @ts-expect-error seats must be a number.
setField(form, "seats", "three");

console.log(updated.seats.value);

O cast depois de Object.fromEntries é o ponto de revisão. Ele não confia em entrada externa; ele documenta que a transformação preserva as mesmas chaves, embora TypeScript não consiga inferir o mapped type exato. Peça ao Claude Code para justificar cada cast.

Verificação com tsc e testes de tipo

Um artigo sobre generics precisa compilar. Coloque os exemplos em examples/generics.ts e rode uma checagem estrita.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noEmit": true,
    "lib": ["ES2022", "DOM"]
  },
  "include": ["examples/**/*.ts"]
}
npm install --save-dev typescript
npx tsc --noEmit --strict --lib ES2022,DOM examples/generics.ts

Para testar apenas tipos, use pequenas assertions de compilação. Elas não rodam em JavaScript, mas quebram a build se o tipo esperado mudar.

type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2)
    ? true
    : false;

type Expect<T extends true> = T;

type PickReadonly<T, K extends keyof T> = {
  readonly [P in K]: T[P];
};

type Account = {
  id: string;
  email: string;
  seats: number;
};

type PublicAccount = PickReadonly<Account, "id" | "email">;

type PublicAccountCheck = Expect<
  Equal<PublicAccount, { readonly id: string; readonly email: string }>
>;

const leaked: PublicAccount = {
  id: "a_1",
  email: "team@example.com",
  // @ts-expect-error seats is intentionally not part of PublicAccount.
  seats: 10,
};

console.log("Type checks are compile-time only.");

Templates de revisão para Claude Code

Depois de gerar, use Claude Code para revisar tipos com foco.

Template 1: revisão de helper genérico
Revise esta função TypeScript.
Objetivo: remover duplicados de um array por uma chave escolhida.
Condições: key deve ser K extends keyof T. Proibido any. Inclua @ts-expect-error para chave inexistente.
Saída: problemas, código corrigido e comando tsc para verificar.
Template 2: revisão de resposta API
Revise este tipo de resposta API.
Objetivo: sucesso tem data; falha tem error.
Condições: evite campos opcionais vagos como data?: T. Confirme que ok estreita o tipo.
Saída: exemplo seguro de caller, exemplo de falha e testes de tipo extras.
Template 3: revisão de mapped types
Revise este mapped type.
Objetivo: derivar estado de campos a partir de um modelo de formulário.
Condições: explique keyof, T[K], readonly, optional properties e qualquer cast necessário.
Saída: fluxo de tipos, casos frágeis e menor correção segura.
Template 4: auditoria antes do PR
Audite os generics, conditional types e mapped types deste diff.
Verifique: any, Record amplo demais, parâmetros de tipo desnecessários, falta de @ts-expect-error e falta de validação runtime.
Saída: bloqueadores, melhorias menores e testes extras por prioridade.

Armadilhas comuns

ArmadilhaO que quebraHábito mais seguro
Usar any para parecer genéricoO retorno perde informaçãoPreserve a relação com T
Declarar key como stringPropriedades inexistentes compilamUse K extends keyof T
Abusar de Record<string, unknown>Propriedades concretas desaparecemUse object quando não for dicionário
Tornar tudo optional na APICaller não confia em data ou errorUse união discriminada
Cast sem explicaçãoReview não avalia segurançaDocumente a condição antes do cast

A diferença entre T extends object e T extends Record<string, unknown> merece atenção. Um modelo de formulário geralmente só precisa ser objeto. Um helper de dicionário com chaves arbitrárias pode justificar Record.

CTA: conectar segurança de tipos à receita

Generics não são apenas estilo de linguagem. Se tipos falham em formulários, checkout, payloads de API, templates de produto ou eventos de analytics, o caminho do leitor até o cliente pode quebrar. Comece com a cola gratuita de Claude Code, use produtos e templates quando precisar de prompts reutilizáveis, e vá para treinamento ou consultoria Claude Code se a equipe precisa padronizar CLAUDE.md, revisão, CI e rollout.

Ao aplicar os exemplos, identifique primeiro os tipos mais próximos do negócio: conta, faturamento, formulário, resposta API e tracking. Pergunte ao Claude Code não só “compila?”, mas “esse erro de tipo pode quebrar uma conversão?”.

Resultado testado

Ao testar o fluxo, Masa percebeu que separar o prompt de implementação do prompt de revisão de tipos gera resultados mais estáveis. Primeiro o helper é criado; depois Claude Code audita any, falta de keyof, respostas API opcionais demais e ausência de @ts-expect-error. Os exemplos uniqueBy e estado de formulário são úteis porque tsc --noEmit --strict prova os dois lados do contrato: chamadas válidas compilam e chamadas intencionalmente inválidas são rejeitadas.

#Claude Code #TypeScript #generics #type safety #design patterns
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.