//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
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álido// 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>;
\}
\}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\}
/>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">;// 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;
\}// 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>;
\}// 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
\}// 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>
);
\}// 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;// 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
\}
\}any - Usa unknown y estrecha el tipo, o usa un genéricoas para aserciones de tipo - Usa type guards o uniones discriminadassatisfies en lugar de as const cuando necesites inferencia y validación
## 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} />;
}
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 verdadEsta skill proporciona a Claude una biblioteca de patrones completa que cubre:
any, sin as)mkdir -p .claude/skills/typescript-react-patterns
# Pega el contenido de Receta en .claude/skills/typescript-react-patterns/SKILL.mdComponentPropsWithoutRef por defecto. Solo usa ComponentPropsWithRef cuando necesites reenviar refs explícitamente.satisfies valida, pero la variable conserva su tipo inferido, no el tipo comprobado.| 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 |
string)z.infer, ReturnType, typeof)strict: true y noUncheckedIndexedAccess: true)type ButtonProps =
| { variant: "button"; onClick: () => void; href?: never }
| { variant: "link"; href: string; onClick?: never }
| { variant: "submit"; onClick?: never; href?: never };never)href a una variante "button"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>
);
}T se infiere automáticamente a partir del array items que pasa quien invoca el componentetype 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;
}params como searchParams son tipos Promise en Next.js 15+ y deben esperarse con awaitReact.memoT no se conserva a través del límite de memoComponentPropsWithoutRef<"input"> - extrae props sin ref (úsalo por defecto)ComponentPropsWithRef<"input"> - incluye el tipo de refComponentPropsWithRef cuando necesites reenviar refs explícitamentetype 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>;
}ashref es válido cuando as="a")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ónsatisfies valida que un valor coincide con un tipo, pero conserva el tipo inferido (no estrecha)as const hace el valor profundamente readonly con tipos literalessatisfies cuando necesites inferencia y validación a la veztype 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" };
}prevState (tipado como FormState) y formData (FormData)Promise<FormState>, coincidiendo con la forma del state inicialany desactiva toda la comprobación de tipos y deja pasar bugsunknown y estrecha con type guards en lugar de anyas evaden el comprobador de tipos y pueden ocultar erroresRevisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥