Tips & Tricks (Actualizado: 2/6/2026)

Generics de TypeScript con Claude Code: keyof, constraints y API types

Aprende TypeScript Generics con Claude Code: constraints, keyof, mapped types, tipos de API y verificación tsc.

Generics de TypeScript con Claude Code: keyof, constraints y API types

Por qué un prompt vago produce generics frágiles

TypeScript Generics permite que una función, un tipo o una clase funcione con varias formas de datos sin perder la relación entre lo que entra y lo que sale. El problema para principiantes no es la letra T; el problema es pedirle a Claude Code “hazlo genérico” sin explicar qué debe seguir siendo flexible y qué debe quedar limitado. Ese prompt suele producir código que parece reutilizable, pero que se apoya demasiado en any, unknown o Record<string, unknown>.

En un proyecto real, los generics aparecen cerca de respuestas de API, formularios, cuentas, facturación, eventos de analytics y CTA de producto. Si ahí el tipo queda demasiado amplio, el fallo no es solo de autocompletado: una pantalla de consulta, compra o registro puede aceptar datos que no debería. La regla práctica de Masa es pedir siempre un ejemplo válido y un ejemplo que deba fallar en compilación. Si no puedes demostrar el fallo con tsc, el tipo todavía no es una garantía.

El flujo mental de este artículo es:

valor de entrada -> capturado como T -> clave limitada con keyof T -> forma transformada con mapped types -> contrato verificado con tsc

La sintaxis se revisó contra la documentación oficial de TypeScript: Generics, operador keyof, Mapped Types y Conditional Types. Para continuar el flujo de trabajo con Claude Code, también conviene leer tips de TypeScript con Claude Code y utility types con Claude Code.

La tabla de revisión antes de escribir código

Generics es una herramienta de compilación. T no es una variable en tiempo de ejecución, sino un parámetro de tipo que permite al compilador recordar qué tipo entró y qué tipo debe salir. En este contexto, extends significa “acepta solo tipos que cumplan esta forma”. keyof T crea el conjunto de nombres de propiedad disponibles en T. Un mapped type recorre esos nombres y construye un tipo nuevo.

Antes de dejar que Claude Code modifique el repositorio, dale esta clase de contrato:

PreguntaQué decirle a Claude CodeQué revisar
Qué representa TObjeto de dominio, DTO o modelo de formularioEl resultado no pierde el tipo original
Qué debe limitarseK extends keyof T, E extends ApiError, T extends objectLas llamadas inválidas fallan al compilar
Cómo se prueba@ts-expect-error, Expect, comando estricto de tscEl ejemplo malo realmente falla

Esta tabla evita un fallo común: Claude Code escribe código que compila solo porque añade un cast al final. Un cast puede ser válido cuando documenta una transformación que TypeScript no infiere, pero no debe ocultar un diseño demasiado amplio.

Caso 1: eliminar duplicados con una clave segura

El primer caso práctico es uniqueBy, útil para filas de API, importaciones CSV, tablas de administración y listas en UI. Si key se declara como string, cualquiera puede pasar una propiedad que no existe. Con K extends keyof T, la clave tiene que ser una propiedad real del tipo del elemento.

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));

El prompt para Claude Code debe ser concreto: “usa overloads, limita key a keyof T e incluye un @ts-expect-error para una clave inexistente”. Si no lo dices, puede aparecer una versión con key: string y item[key as keyof T], que desplaza el riesgo hacia runtime.

Caso 2: respuestas de API sin optional confuso

El segundo caso es el tipo de respuesta de una API. Muchos equipos escriben data?: T y error?: ApiError en una sola interfaz. Parece rápido, pero obliga al caller a comprobar cada vez si existe data, si existe error o si la respuesta quedó en un estado imposible. Una unión discriminada lo hace explícito: éxito tiene data, fallo tiene error y ok estrecha el 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());

Este patrón funciona bien con Claude Code porque el prompt puede describir la validación de runtime y el contrato de tipos al mismo tiempo. Para diseño backend completo, revisa desarrollo de API con Claude Code y testing de API con Claude Code.

Caso 3: estado de formulario con mapped types

El tercer caso es un estado de formulario. Partimos de un modelo de negocio y derivamos para cada campo un estado con value, dirty y errors. Con mapped types no duplicamos nombres de campos, y el tipo del valor se mantiene: email sigue siendo string, seats sigue siendo number y newsletter sigue siendo 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);

El punto delicado es el cast después de Object.fromEntries. No estamos confiando en datos externos; estamos documentando que la transformación conserva las mismas claves aunque TypeScript no pueda inferir el mapped type exacto. Pídele a Claude Code que explique todo cast que agregue.

Verificación con tsc y type tests

Un artículo sobre generics debe compilar. Coloca los ejemplos en examples/generics.ts y ejecuta una comprobación estricta.

{
  "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 pruebas solo de tipos, usa pequeñas aserciones de compilación. No hacen nada en JavaScript, pero fallan si el tipo resultante cambia.

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.");

Plantillas para revisar tipos con Claude Code

Generar código es solo la mitad. Después pide una revisión centrada en tipos.

Plantilla 1: revisión de helper genérico
Revisa esta función TypeScript.
Objetivo: eliminar duplicados de un array por una clave seleccionada.
Condiciones: key debe ser K extends keyof T. Prohibido any. Incluye @ts-expect-error para una clave inexistente.
Salida: problemas, versión corregida y comando tsc para verificar.
Plantilla 2: revisión de respuesta API
Revisa este tipo de respuesta API.
Objetivo: éxito tiene data; fallo tiene error.
Condiciones: evita campos opcionales vagos como data?: T. Confirma que ok estrecha el tipo.
Salida: ejemplo seguro de uso, ejemplo de fallo y type tests adicionales.
Plantilla 3: revisión de mapped types
Revisa este mapped type.
Objetivo: derivar estado de campos desde un modelo de formulario.
Condiciones: explica keyof, T[K], readonly, optional properties y cualquier cast necesario.
Salida: flujo de tipos, casos frágiles y corrección mínima.
Plantilla 4: auditoría antes del PR
Audita los generics, conditional types y mapped types de este diff.
Revisa: any, Record demasiado amplio, parámetros de tipo innecesarios, falta de @ts-expect-error y falta de validación runtime.
Salida: bloqueadores, mejoras menores y tests extra por prioridad.

Errores frecuentes que debes detectar

ErrorQué se rompeHábito más seguro
Usar any para parecer genéricoSe pierde información del retornoCaptura la relación con T
Escribir key como stringCompilan propiedades inexistentesUsa K extends keyof T
Abusar de Record<string, unknown>Desaparecen propiedades concretasUsa object si no necesitas diccionario
Hacer opcionales todos los campos APIEl caller no puede confiar en data o errorUsa unión discriminada
Casts sin explicaciónNadie puede evaluar la seguridadDocumenta la condición que lo hace válido

La diferencia entre T extends object y T extends Record<string, unknown> es importante. Un formulario normalmente solo necesita ser un objeto. Un helper de diccionario con claves arbitrarias sí puede necesitar Record.

CTA: conectar tipos con rutas de ingresos

Generics no es solo elegancia técnica. Si los tipos fallan en formularios, checkout, payloads de API, plantillas de producto o eventos de analytics, se rompe el camino desde lector hasta cliente. Empieza con la chuleta gratuita de Claude Code, usa productos y plantillas cuando necesites prompts reutilizables, y lleva el flujo a formación y consultoría Claude Code cuando el equipo necesite CLAUDE.md, reglas de revisión, CI y rollout.

Al aplicar estos ejemplos, identifica primero los tipos más cercanos al negocio: cuenta, facturación, formulario, respuesta API y tracking. Pide a Claude Code no solo “¿compila?”, sino “¿este error de tipo puede romper una conversión?”.

Resultado al probarlo

Al probar este flujo, Masa encontró que separar el prompt de implementación del prompt de revisión de tipos da resultados más estables. Primero se genera el helper; luego se audita any, falta de keyof, respuestas API demasiado opcionales y ausencia de @ts-expect-error. Los ejemplos de uniqueBy y estado de formulario son útiles porque tsc --noEmit --strict demuestra ambos lados del contrato: las llamadas válidas compilan y las inválidas se rechazan.

#Claude Code #TypeScript #generics #type safety #design patterns
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.