Aprende a tipar correctamente props, hooks y contextos en TypeScript y React
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 anyy prefiere contratos claros. - Props y refs: evita
React.FC, usa referencias DOM connullinicial y valores mutables con valor inicial. - Contextos seguros: inicializa con
nully 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
nully 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
typeconexport typecuando sean solo contratos. Eso deja claro que no hay runtime. - No uses
aspara “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.infersi 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 anysalvo 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:
- Revisa contextos: elimina
asy añade hooks defensivos. - Estándariza useRef y useState según lo explicado.
- Añade
displayNamea 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?
- ¿Cuándo especificar el tipo en useState?
- ¿Cómo tipar correctamente useRef para DOM?
- ¿Qué hacer si alguien usa un context fuera del provider?
- ¿Es aceptable usar
asen alguna situación? - ¿Cómo mejorar la validación de APIs y mantener tipos?
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.
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).
Inicializa la ref con null y usa el tipo del elemento: useRef<HTMLInputElement | null>(null). Accede con optional chaining (inputRef.current?.focus()).
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')).
as en alguna situación?
Evita as salvo casos documentados y justificables. Usarlo para “callar” al compilador oculta problemas que aparecerán en runtime.
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.
