Skill de Patrones TypeScript en React - Una receta de skill de Claude Code para patrones avanzados de TypeScript en React y Next.js
Estas recetas de skills están diseñadas para Claude Code, pero también funcionan con otros agentes de codificación con IA que admitan archivos de skill/instrucciones.
Receta
El contenido completo de SKILL.md que puedes copiar en .claude/skills/typescript-react-patterns/SKILL.md:
---
name: typescript-react-patterns
description: "Patrones avanzados de TypeScript para componentes de React y Next.js. Úsalo cuando te pidan: tipar este componente, ayuda con TypeScript, componente genérico, tipar props, uniones discriminadas, utility types, tipar hooks, tipar Server Components."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Patrones TypeScript en React
Eres un experto en TypeScript especializado en patrones de React y Next.js. Proporciona los tipos más precisos y estrictos para cada escenario.
## Principios fundamentales
1. **Prefiere tipos estrechos frente a amplios** - Usa literales de string en lugar de string, objetos específicos en lugar de Record
2. **Usa uniones discriminadas para el renderizado condicional** - Nunca dependas de optional chaining para atravesar props de variantes
3. **Deriva tipos a partir de datos** - Usa typeof, ReturnType e inferencia con Zod en lugar de declaraciones de tipos manuales
4. **Modo estricto siempre** - Activa strict: true y noUncheckedIndexedAccess: true
## Biblioteca de patrones
### 1. Componente polimórfico (prop as)
```tsx
type PolymorphicProps<E extends React.ElementType> = \{
as?: E;
children: React.ReactNode;
\} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">(\{
as,
children,
...props
\}: PolymorphicProps<E>) \{
const Component = as ?? "div";
return <Component \{...props\}>\{children\}</Component>;
\}
// Uso — totalmente tipado
<Box as="a" href="/about">Enlace</Box> // href es válido
<Box as="button" onClick=\{handleClick\}>Ir</Box> // onClick es válido2. Props con unión discriminada
// En lugar de props opcionales que dependen unas de otras:
// MAL
type BadProps = \{ variant?: "link"; href?: string; onClick?: () => void \};
// BIEN — unión discriminada
type ButtonProps =
| \{ variant: "button"; onClick: () => void; href?: never \}
| \{ variant: "link"; href: string; onClick?: never \}
| \{ variant: "submit"; onClick?: never; href?: never \};
function Action(props: ButtonProps) \{
switch (props.variant) \{
case "button":
return <button onClick=\{props.onClick\}>Haz clic</button>;
case "link":
return <a href=\{props.href\}>Enlace</a>;
case "submit":
return <button type="submit">Enviar</button>;
\}
\}3. Componente List genérico
type ListProps<T> = \{
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
emptyMessage?: string;
\};
function List<T>(\{ items, renderItem, keyExtractor, emptyMessage \}: ListProps<T>) \{
if (items.length === 0) \{
return <p>\{emptyMessage ?? "Sin elementos"\}</p>;
\}
return (
<ul>
\{items.map((item, i) => (
<li key=\{keyExtractor(item)\}>\{renderItem(item, i)\}</li>
))\}
</ul>
);
\}
// Uso — T se infiere a partir de items
<List
items=\{users\}
renderItem=\{(user) => <span>\{user.name\}</span>\} // user está tipado como User
keyExtractor=\{(user) => user.id\}
/>4. Extracción de props de componentes
import type \{ ComponentProps, ComponentRef \} from "react";
// Extrae props de cualquier componente
type InputProps = ComponentProps<"input">;
type ButtonProps = ComponentProps<typeof Button>;
// Extrae el tipo de ref
type InputRef = ComponentRef<"input">; // HTMLInputElement
// Selecciona props específicas
type PartialInputProps = Pick<ComponentProps<"input">, "value" | "onChange" | "placeholder">;5. Tipado de hooks personalizados
// Devuelve tupla (como useState)
function useToggle(initial = false) \{
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return [value, \{ toggle, setTrue, setFalse \}] as const;
\}
// Tipo de retorno: readonly [boolean, \{ toggle, setTrue, setFalse \}]
// Hook genérico
function useLocalStorage<T>(key: string, initialValue: T) \{
const [stored, setStored] = useState<T>(() => \{
if (typeof window === "undefined") return initialValue;
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
\});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => \{
setStored((prev) => \{
const next = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(next));
return next;
\});
\},
[key]
);
return [stored, setValue] as const;
\}6. Tipado de Server Components
// Props de Server Component — params y searchParams son Promises en Next.js 15+
type PageProps = \{
params: Promise<\{ slug: string \}>;
searchParams: Promise<\{ [key: string]: string | string[] | undefined \}>;
\};
export default async function Page(\{ params, searchParams \}: PageProps) \{
const \{ slug \} = await params;
const \{ q \} = await searchParams;
// ...
\}
// Props de Layout
type LayoutProps = \{
children: React.ReactNode;
params: Promise<\{ slug: string \}>;
\};
export default async function Layout(\{ children, params \}: LayoutProps) \{
const \{ slug \} = await params;
return <div>\{children\}</div>;
\}7. Tipado de Server Actions
// Server Action con state tipado
type FormState = \{
errors?: \{
name?: string[];
email?: string[];
\};
message?: string;
success: boolean;
\};
export async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> \{
// validar y procesar
return \{ success: true, message: "Usuario creado" \};
\}
// Componente cliente que usa la action
"use client";
import \{ useActionState \} from "react";
function Form() \{
const [state, action, pending] = useActionState(createUser, \{
success: false,
\});
// state está tipado como FormState
\}8. Manejadores de eventos estrictos
// Tipos de evento específicos en lugar de React.SyntheticEvent genérico
function Form() \{
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => \{
console.log(e.target.value); // string
\};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => \{
e.preventDefault();
const formData = new FormData(e.currentTarget);
\};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => \{
if (e.key === "Enter") submit();
\};
return (
<form onSubmit=\{handleSubmit\}>
<input onChange=\{handleChange\} onKeyDown=\{handleKeyDown\} />
</form>
);
\}9. Recetas de utility types
// Hace obligatorias claves específicas
type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
// Hace opcionales claves específicas
type WithOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Partial profundo
type DeepPartial<T> = \{
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
\};
// Omit estricto (error si la clave no existe)
type StrictOmit<T, K extends keyof T> = Omit<T, K>;
// Extrae un miembro de una unión
type ExtractMember<T, U> = T extends U ? T : never;10. Patrones de estrechamiento de tipos
// Función type guard
function isError(result: SuccessResult | ErrorResult): result is ErrorResult \{
return "error" in result;
\}
// Switch exhaustivo
function assertNever(value: never): never \{
throw new Error(`Unexpected value: ${value}`);
\}
function handleStatus(status: "idle" | "loading" | "success" | "error") \{
switch (status) \{
case "idle": return null;
case "loading": return <Spinner />;
case "success": return <Done />;
case "error": return <Error />;
default: return assertNever(status); // error de compilación si falta un case
\}
\}Reglas
- Nunca uses
any- Usaunknowny estrecha el tipo, o usa un genérico - Nunca uses
aspara aserciones de tipo - Usa type guards o uniones discriminadas - Tipa siempre los valores de retorno de funciones exportadas - Ayuda a detectar desviaciones pronto
- Usa
satisfiesen lugar deas constcuando necesites inferencia y validación - Prefiere interfaces para objetos, types para uniones - Las interfaces tienen mejores mensajes de error y son extensibles
## Ejemplo en funcionamiento
### Ejemplo 1: El usuario pregunta «¿Cómo tipar un componente que puede ser un botón o un enlace?»
**Prompt del usuario:** «Necesito un componente Button que se renderice como enlace cuando se proporciona href.»
**La respuesta guiada por la skill produciría:**
```tsx
type ButtonBaseProps = {
children: React.ReactNode;
size?: "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "ghost";
};
type ButtonAsButton = ButtonBaseProps &
Omit<React.ComponentPropsWithoutRef<"button">, keyof ButtonBaseProps> & {
href?: never;
};
type ButtonAsLink = ButtonBaseProps &
Omit<React.ComponentPropsWithoutRef<"a">, keyof ButtonBaseProps> & {
href: string;
};
type ButtonProps = ButtonAsButton | ButtonAsLink;
function Button(props: ButtonProps) {
if (props.href !== undefined) {
const { size, variant, ...rest } = props;
return <a className={getStyles(size, variant)} {...rest} />;
}
const { size, variant, ...rest } = props;
return <button className={getStyles(size, variant)} {...rest} />;
}
Ejemplo 2: El usuario pregunta «Tipa mi respuesta de API»
La respuesta guiada por la skill usaría inferencia con Zod:
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "user", "moderator"]),
});
type User = z.infer<typeof UserSchema>;
// Ahora el tipo User se deriva del schema — única fuente de verdadProfundización
Cómo funciona la skill
Esta skill proporciona a Claude una biblioteca de patrones completa que cubre:
- 10 recetas de TypeScript - Patrones listos para usar en retos comunes de tipado en React
- Reglas estrictas - Convenciones que exigen seguridad de tipos (sin
any, sinas) - Tipado específico de Next.js - Server Components, Server Actions y props de página
- Kit de utility types - Utility types personalizados para transformaciones habituales
Personalización
- Añade las convenciones de props de componentes específicas de tu proyecto
- Incluye utility types personalizados usados en tu codebase
- Especifica los requisitos de rigor del tsconfig
- Añade patrones para tu librería de UI concreta (p. ej., Radix, Headless UI)
Cómo instalar
mkdir -p .claude/skills/typescript-react-patterns
# Pega el contenido de Receta en .claude/skills/typescript-react-patterns/SKILL.mdErrores comunes
- ComponentProps vs ComponentPropsWithRef - Usa
ComponentPropsWithoutRefpor defecto. Solo usaComponentPropsWithRefcuando necesites reenviar refs explícitamente. - Los componentes genéricos pierden inferencia con React.memo - Envuelve la llamada a memo en una aserción de tipo o usa otra estrategia de memoización.
- satisfies no estrecha el tipo -
satisfiesvalida, pero la variable conserva su tipo inferido, no el tipo comprobado. - La inferencia con Zod es unidireccional - Los cambios en el schema de Zod actualizan el tipo automáticamente, pero TypeScript no puede validar que el schema coincida con un tipo definido por separado.
Alternativas
| Enfoque | Cuándo usarlo |
|---|---|
| Tipos JSDoc | Proyectos que no pueden adoptar TypeScript |
| io-ts | Validación en tiempo de ejecución con integración fp-ts |
| Valibot | Alternativa a Zod con bundle más pequeño |
| ArkType | Validación de schemas más rápida con sintaxis nativa de TypeScript |
Preguntas frecuentes
¿Cuáles son los cuatro principios fundamentales de TypeScript descritos en esta página?
- Prefiere tipos estrechos frente a amplios (literales de string en lugar de
string) - Usa uniones discriminadas para el renderizado condicional
- Deriva tipos a partir de datos (
z.infer,ReturnType,typeof) - Activa el modo estricto (
strict: trueynoUncheckedIndexedAccess: true)
¿Cómo evita una unión discriminada combinaciones inválidas de props?
type ButtonProps =
| { variant: "button"; onClick: () => void; href?: never }
| { variant: "link"; href: string; onClick?: never }
| { variant: "submit"; onClick?: never; href?: never };- Cada variante define exactamente qué props son obligatorias y cuáles están prohibidas (
never) - TypeScript marca error en tiempo de compilación si pasas
hrefa una variante "button"
¿Cómo crear un componente List genérico que infiera los tipos de los elementos?
type ListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
};
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}Tse infiere automáticamente a partir del arrayitemsque pasa quien invoca el componente
¿Cómo tipar params y searchParams en componentes de página de Next.js 15?
type PageProps = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
export default async function Page({ params, searchParams }: PageProps) {
const { slug } = await params;
const { q } = await searchParams;
}- Tanto
paramscomosearchParamsson tiposPromiseen Next.js 15+ y deben esperarse con await
Error común: ¿Qué ocurre cuando envuelves un componente genérico en React.memo?
- Los componentes genéricos pierden su inferencia de tipos al envolverse en
React.memo - El parámetro genérico
Tno se conserva a través del límite de memo - Usa una aserción de tipo en la llamada a memo o busca una estrategia de memoización alternativa
¿Cuál es la diferencia entre ComponentProps, ComponentPropsWithRef y ComponentPropsWithoutRef?
ComponentPropsWithoutRef<"input">— extrae props sin ref (úsalo por defecto)ComponentPropsWithRef<"input">— incluye el tipo de ref- Solo usa
ComponentPropsWithRefcuando necesites reenviar refs explícitamente
¿Cómo funciona el patrón polimórfico con prop "as"?
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as, children, ...props
}: PolymorphicProps<E>) {
const Component = as ?? "div";
return <Component {...props}>{children}</Component>;
}- El componente se renderiza como el elemento que se pasa mediante
as - Las props quedan totalmente tipadas para ese elemento (p. ej.,
hrefes válido cuandoas="a")
¿Qué recetas de utility types proporciona esta página?
WithRequired<T, K>— hace obligatorias claves específicasWithOptional<T, K>— hace opcionales claves específicasDeepPartial<T>— hace opcionales todas las claves de forma recursivaStrictOmit<T, K>— Omit que marca error si la clave no existeExtractMember<T, U>— extrae un miembro concreto de una unión
Error común: ¿Cuál es la diferencia entre satisfies y as const?
satisfiesvalida que un valor coincide con un tipo, pero conserva el tipo inferido (no estrecha)as consthace el valor profundamente readonly con tipos literales- Usa
satisfiescuando necesites inferencia y validación a la vez
¿Cómo tipar una Server Action con un retorno de state tipado?
type FormState = {
errors?: { name?: string[]; email?: string[] };
message?: string;
success: boolean;
};
export async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> {
return { success: true, message: "Usuario creado" };
}- La action recibe
prevState(tipado comoFormState) yformData(FormData) - El tipo de retorno es
Promise<FormState>, coincidiendo con la forma del state inicial
¿Por qué nunca deberías usar any o aserciones de tipo as?
anydesactiva toda la comprobación de tipos y deja pasar bugs- Usa
unknowny estrecha con type guards en lugar deany - Las aserciones
asevaden el comprobador de tipos y pueden ocultar errores - Usa uniones discriminadas o type guards para estrechar tipos de forma segura