Una vez dominados los tipos básicos de React, el siguiente nivel es tipar patrones de composición complejos: contextos con valores nulos seguros, componentes que aceptan parámetros de tipo genéricos, compound components que comparten estado interno sin exponerlo al exterior y componentes polimórficos cuya etiqueta HTML se puede cambiar con una prop. Estos patrones aparecen en librerías de UI como Radix, HeadlessUI o Shadcn, y entenderlos permite tanto usarlos mejor como replicarlos en código propio.
Context tipado con type guards
El patrón más seguro para un contexto de React es crear un hook personalizado que lanza un error si se usa fuera del proveedor:
interface AuthContextTipo {
usuario: { id: number; email: string; rol: string };
cerrarSesion(): void;
actualizarPerfil(datos: Partial<{ email: string }>): Promise<void>;
}
const AuthContext = React.createContext<AuthContextTipo | null>(null);
export function AuthProvider({ children }: React.PropsWithChildren) {
const [usuario, setUsuario] = React.useState<AuthContextTipo["usuario"]>(
{ id: 1, email: "[email protected]", rol: "admin" }
);
const valor: AuthContextTipo = {
usuario,
cerrarSesion: () => { /* ... */ },
actualizarPerfil: async (datos) => { setUsuario(u => ({ ...u, ...datos })); },
};
return <AuthContext.Provider value={valor}>{children}</AuthContext.Provider>;
}
// Hook con type guard integrado:
export function useAuth(): AuthContextTipo {
const ctx = React.useContext(AuthContext);
if (ctx === null) {
throw new Error("useAuth debe usarse dentro de <AuthProvider>");
}
return ctx;
}
// Uso:
function Perfil() {
const { usuario, cerrarSesion } = useAuth(); // siempre tipado, nunca null
return <div>{usuario.email}</div>;
}
Componentes genéricos
Un componente genérico acepta parámetros de tipo que se determinan en el punto de uso. En JSX la sintaxis es <Componente<Tipo> /> o se infiere del valor de las props:
interface ListaProps<T> {
items: T[];
renderItem: (item: T, indice: number) => React.ReactNode;
clave: keyof T;
}
function Lista<T extends { [K in keyof T]: unknown }>({
items,
renderItem,
clave,
}: ListaProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={String(item[clave])}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// TypeScript infiere T = Producto:
<Lista
items={productos}
clave="id"
renderItem={(p) => <span>{p.nombre}: {p.precio}</span>}
/>
Compound components con contexto privado
Los compound components son componentes que funcionan juntos y comparten estado interno a través de un contexto, pero sin exponer ese contexto al exterior:
interface TabsContextTipo {
activo: string;
setActivo: (id: string) => void;
}
const TabsContext = React.createContext<TabsContextTipo | null>(null);
function useTabsContext() {
const ctx = React.useContext(TabsContext);
if (!ctx) throw new Error("Debe estar dentro de <Tabs>");
return ctx;
}
function Tabs({ children, defaultTab }: React.PropsWithChildren<{ defaultTab: string }>) {
const [activo, setActivo] = React.useState(defaultTab);
return (
<TabsContext.Provider value={{ activo, setActivo }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
}
function Tab({ id, children }: React.PropsWithChildren<{ id: string }>) {
const { activo, setActivo } = useTabsContext();
return (
<button
className={activo === id ? "activo" : ""}
onClick={() => setActivo(id)}
>{children}</button>
);
}
function TabPanel({ id, children }: React.PropsWithChildren<{ id: string }>) {
const { activo } = useTabsContext();
if (activo !== id) return null;
return <div>{children}</div>;
}
// API de uso limpia:
<Tabs defaultTab="inicio">
<Tab id="inicio">Inicio</Tab>
<Tab id="perfil">Perfil</Tab>
<TabPanel id="inicio">Contenido inicio</TabPanel>
<TabPanel id="perfil">Contenido perfil</TabPanel>
</Tabs>
Componentes polimórficos con la prop as
// Tipo auxiliar para componentes polimórficos:
type PropsMas<E extends React.ElementType, P> =
P & Omit<React.ComponentPropsWithRef<E>, keyof P> & { as?: E };
interface TextoBaseProps {
size?: "sm" | "md" | "lg";
color?: "gris" | "azul" | "rojo";
}
function Texto<E extends React.ElementType = "p">({
as,
size = "md",
color = "gris",
...resto
}: PropsMas<E, TextoBaseProps>) {
const Etiqueta = (as ?? "p") as React.ElementType;
return <Etiqueta className={`texto-${size} color-${color}`} {...resto} />;
}
// Uso:
<Texto>Párrafo por defecto</Texto>
<Texto as="h1" size="lg">Título</Texto>
<Texto as="a" href="/perfil" color="azul">Enlace</Texto>
// TypeScript autocompletará las props del elemento que indiques en as
Estos patrones avanzados son lo que separa una biblioteca de componentes bien tipada de una colección de componentes con any dispersos. Cuando el sistema de tipos entiende la composición del componente, el desarrollador que lo usa recibe autocompletado correcto, errores en tiempo de desarrollo y ningún as unknown as T en el código.
Imagen: Pexels / Pixabay
