Advanced TypeScript Patterns for Scalable React
15/06/2024 - 16:20
Escribir TypeScript en React va más allá de definir interfaces básicas. Para bibliotecas de componentes o aplicaciones de gran escala, necesitamos aprovechar el sistema de tipos de Turing-completo de TS.
1. Componentes Polimórficos con forwardRef
El desafío clásico: crear un componente Button que pueda renderizarse como un a (enlace) o un button, manteniendo acceso a las props nativas correctas y reenviando la ref adecuadamente.
import { ComponentPropsWithRef, ElementType, forwardRef } from 'react';
type PolymorphicProps<C extends ElementType> = {
as?: C;
} & ComponentPropsWithRef<C>;
export const Box = forwardRef(
<C extends ElementType = 'div'>(
{ as, ...props }: PolymorphicProps<C>,
ref: ComponentPropsWithRef<C>['ref']
) => {
const Component = as || 'div';
return <Component ref={ref} {...props} />;
}
);
Nota: Este patrón asegura que si as="a", TS exigirá href si es obligatorio, y rechazará props de button como disabled si no son válidos para un anchor.
2. Inferencia de Tipos (Infer) en Custom Hooks
A veces necesitamos extraer el tipo de retorno de una función asíncrona dentro de un hook genérico. infer es nuestro mejor aliado.
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : any;
// Uso práctico: tipar automáticamente el estado basado en una función de servicio
function useAsyncResource<T extends (...args: any) => Promise<any>>(apiCall: T) {
const [data, setData] = useState<AsyncReturnType<T> | null>(null);
// ...
}
3. Exhaustive Matching con Discriminated Unions
Para manejar estados de UI complejos, evita los booleanos múltiples (isLoading, isError, isSuccess). Usa uniones discriminadas.
type UIState =
| { status: 'IDLE' }
| { status: 'LOADING' }
| { status: 'SUCCESS'; data: User[] }
| { status: 'ERROR'; error: Error };
// TypeScript te obligará a comprobar 'status' antes de acceder a 'data'
function render(state: UIState) {
if (state.status === 'SUCCESS') {
return <List items={state.data} />; // Seguro accesar .data aquí
}
// return ...
}
