Back
Advanced TypeScript Patterns for Scalable React

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 ...
}