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.
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:
| Pregunta | Qué decirle a Claude Code | Qué revisar |
|---|---|---|
Qué representa T | Objeto de dominio, DTO o modelo de formulario | El resultado no pierde el tipo original |
| Qué debe limitarse | K extends keyof T, E extends ApiError, T extends object | Las llamadas inválidas fallan al compilar |
| Cómo se prueba | @ts-expect-error, Expect, comando estricto de tsc | El 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
| Error | Qué se rompe | Hábito más seguro |
|---|---|---|
Usar any para parecer genérico | Se pierde información del retorno | Captura la relación con T |
Escribir key como string | Compilan propiedades inexistentes | Usa K extends keyof T |
Abusar de Record<string, unknown> | Desaparecen propiedades concretas | Usa object si no necesitas diccionario |
| Hacer opcionales todos los campos API | El caller no puede confiar en data o error | Usa unión discriminada |
| Casts sin explicación | Nadie puede evaluar la seguridad | Documenta 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.
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.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Escalera de permisos de Claude Code para ampliar acceso sin perder control
Pasa de read-only a ediciones limitadas, comandos de prueba y checks de deploy con menos riesgo.
Claude Code Small PR Proof Pack: cambios pequeños que sí se pueden revisar
Un paquete de prueba para PRs de Claude Code: diff, checks, URL pública, CTA y rollback.
Gate de revisión antes del commit con Claude Code
Cómo revisar con Claude Code antes del commit: diff, build, URL pública, Gumroad, consultoría, tests y archivos ajenos.