Aprende a tipar correctamente props, hooks y contextos en TypeScript y React

tipar-props-hooks-contextos

TypeScript + React: cómo tipar correctamente props, hooks y contextos

Tiempo estimado de lectura: 4 min

  • Tipado explícito evita errores silenciosos: evita atajos como as any y prefiere contratos claros.
  • Props y refs: evita React.FC, usa referencias DOM con null inicial y valores mutables con valor inicial.
  • Contextos seguros: inicializa con null y expón hooks que hagan fail-fast si se usan fuera del provider.
  • Handlers y hooks: aprovecha los tipos de React (ChangeEvent, FormEvent) y deja que TS infiera cuando sea seguro.

¿Quieres dejar de parchear bugs con as any y que tu base de código deje de tener sorpresas en producción? Bien. Esto es lo que realmente necesitas saber sobre TypeScript + React: cómo tipar correctamente props, hooks y contextos. No es teoría. Son patrones que evitan errores silenciosos, mejoran el autocompletado y hacen que el código sea mantenible cuando el equipo crece.

Resumen rápido (lectores con prisa)

Tipar React con TypeScript reduce errores en producción y mejora DX. Evita React.FC, inicializa contextos con null y valida con hooks, usa refs con null para DOM y valores iniciales para mutables, y aprovecha los tipos sintéticos de eventos de React.

Evita React.FC: tipa los parámetros explícitamente

React.FC fue útil en tutoriales, pero introduce problemas: children implícitos, genéricos torpes y ruido. Tipar la función es más claro y explícito.

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  children?: React.ReactNode;
}

export function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
  return <button className={`btn-${variant}`} onClick={onClick}>{children ?? label}</button>;
}

React.ReactNode cubre todo lo que necesitas para children. Punto.

useState: deja que TS infiera cuando pueda, explícito cuando haga falta

Si el estado empieza con un primitivo, no especifiques el tipo. Si empieza vacío y luego será un objeto, usa una unión con null.

interface User { id: string; email: string; }

const [count, setCount] = useState(0);            // OK, inferido
const [user, setUser] = useState<User | null>(null); // OK, explícito

¿Por qué? Porque evitarás tener que castear más adelante y te proteges contra undefined al acceder a propiedades.

useRef: dos usos, dos reglas

useRef sirve para referencias DOM y para valores mutables que no disparan re-render. Los tipos cambian según el valor inicial.

  • DOM refs: inicializa con null y maneja optional chaining.
  • Valores mutables: inicializa con el valor y muta .current.
const inputRef = useRef<HTMLInputElement | null>(null);
const renderCount = useRef(0);

inputRef.current?.focus();
renderCount.current += 1;

No uses as para saltarte el null check. Esa falsedad te estallará en runtime.

Eventos del DOM: tipa cada handler

No uses any. React expone tipos sintéticos bien definidos. Úsalos y disfruta del autocompletado.

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};

Esto evita errores tontos como leer propiedades inexistentes.

useContext: seguro, explícito y con fail-fast

No hagas createContext({} as ThemeContext). Ese as silencia al compilador y deja el error para producción.

Patrón correcto: contexto con null y hook personalizado que comprueba la presencia del provider.

interface ThemeContextType { theme: 'light'|'dark'; toggle: () => void; }
const ThemeContext = createContext<ThemeContextType | null>(null);

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme debe usarse dentro de ThemeProvider');
  return ctx;
}

Fail-fast: si alguien usa el hook fuera del provider, fallas rápido y el stack trace dice dónde.

forwardRef: firma invertida, atención al tipo genérico

La firma de forwardRef es contraintuitiva: el primer genérico es el tipo de ref, el segundo las props.

interface InputProps { label: string }

export const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ label, ...props }, ref) => (
  <label>
    {label}
    <input ref={ref} {...props} />
  </label>
));
CustomInput.displayName = 'CustomInput';

Siempre define displayName para facilitar debugging en React DevTools.

Tips prácticos que cambian proyectos

  • Exporta type con export type cuando sean solo contratos. Eso deja claro que no hay runtime.
  • No uses as para “callar” al compilador. Es un atajo que se vuelve deuda.
  • Si necesitas tipos genéricos en componentes, tipa explícitamente props y evita React.FC.
  • Para APIs y carga asíncrona, combina Zod (o similar) con z.infer si necesitas validación runtime y tipos derivados.

Checklist rápido antes de push

  • ¿Contextos inicializados con null y validados por hooks? ✔
  • ¿useRef con null para DOM y con valor inicial para mutables? ✔
  • ¿Handlers con React.ChangeEvent / FormEvent? ✔
  • ¿No hay as any salvo casos documentados? ✔

Cierra con criterio

Tipar React no es un ejercicio académico. Es la forma más barata de prevenir fallos en producción y mejorar el DX de tu equipo. Haz estas tres cosas hoy:

  1. Revisa contextos: elimina as y añade hooks defensivos.
  2. Estándariza useRef y useState según lo explicado.
  3. Añade displayName a los componentes con forwardRef.

Aplica esto en tu repo. Si algo rompe después, sabrás exactamente por qué. Esto no acaba aquí. Hay más patrones (componentes polimórficos, inferencia con generics, overloads en hooks) que merecen otra nota.

FAQ

¿Por qué evitar React.FC?

Porque introduce children implícitos, dificulta genéricos y añade ruido. Tipar explícitamente los parámetros es más claro y evita sorpresas.

¿Cuándo especificar el tipo en useState?

No lo especifiques si el estado inicia con un primitivo (deja que TS infiera). Si el estado inicia vacío y luego será un objeto, usa una unión con null (por ejemplo User | null).

¿Cómo tipar correctamente useRef para DOM?

Inicializa la ref con null y usa el tipo del elemento: useRef<HTMLInputElement | null>(null). Accede con optional chaining (inputRef.current?.focus()).

¿Qué hacer si alguien usa un context fuera del provider?

Exponer un hook que haga fail-fast: si el contexto es null, lanzar un error claro (por ejemplo throw new Error('useTheme debe usarse dentro de ThemeProvider')).

¿Es aceptable usar as en alguna situación?

Evita as salvo casos documentados y justificables. Usarlo para “callar” al compilador oculta problemas que aparecerán en runtime.

¿Cómo mejorar la validación de APIs y mantener tipos?

Combina validación runtime con librerías como Zod y usa z.infer para derivar tipos TypeScript a partir de los esquemas de validación.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *