
Componentes funcionales tipados
En React con TypeScript, los componentes funcionales se tipan declarando una interfaz para sus props. La convención más extendida es usar una interfaz con el nombre del componente seguido de Props:
interface BotonProps {
texto: string;
variante?: "primary" | "secondary" | "danger";
deshabilitado?: boolean;
onClick: () => void;
children?: React.ReactNode;
}
function Boton({
texto,
variante = "primary",
deshabilitado = false,
onClick,
children
}: BotonProps) {
return (
<button
className={`btn btn-${variante}`}
disabled={deshabilitado}
onClick={onClick}
>
{texto}
{children}
</button>
);
}
El tipo React.ReactNode es el más amplio para children: acepta elementos JSX, strings, números, arrays y nulos. Para restricciones más estrictas puedes usar React.ReactElement (solo JSX) o tipos concretos.
Tipos de retorno de componentes
Aunque TypeScript puede inferir el tipo de retorno de un componente, en algunos casos conviene anotarlo explícitamente:
// React.FC es una forma alternativa pero menos recomendada en proyectos modernos
const Tarjeta: React.FC<TarjetaProps> = ({ titulo }) => <div>{titulo}</div>;
// Anotación de retorno explícita (recomendado para componentes complejos)
function Lista({ items }: ListaProps): React.JSX.Element {
return <ul>{items.map(item => <li key={item.id}>{item.nombre}</li>)}</ul>;
}
// Componentes que pueden retornar null
function CargaCondicional({ cargando, children }: Props): React.JSX.Element | null {
if (cargando) return null;
return <>{children}</>;
}
Tipando eventos del DOM
Los eventos en React tienen tipos específicos que incluyen información sobre el elemento que los originó:
// Evento de input
function CampoTexto() {
const [valor, setValor] = React.useState("");
const manejarCambio = (evento: React.ChangeEvent<HTMLInputElement>) => {
setValor(evento.target.value);
};
return <input value={valor} onChange={manejarCambio} />;
}
// Evento de formulario
function Formulario() {
const manejarEnvio = (evento: React.FormEvent<HTMLFormElement>) => {
evento.preventDefault();
// Procesar formulario
};
return <form onSubmit={manejarEnvio}>...</form>;
}
// Evento de click con información del target
function Lista() {
const manejarClick = (evento: React.MouseEvent<HTMLButtonElement>) => {
console.log(evento.currentTarget.dataset.id);
};
return <button data-id="123" onClick={manejarClick}>Ver</button>;
}
useState tipado
TypeScript infiere el tipo del estado a partir del valor inicial. Para tipos complejos o cuando el estado puede ser null, es conveniente anotar explícitamente:
interface Usuario {
id: string;
nombre: string;
email: string;
}
function PerfilUsuario() {
// Estado con valor inicial null: TypeScript infiere Usuario | null
const [usuario, setUsuario] = React.useState<Usuario | null>(null);
// Estado con array vacío: sin anotación TypeScript inferiría never[]
const [items, setItems] = React.useState<string[]>([]);
// Estado con tipo unión
const [estado, setEstado] = React.useState<"cargando" | "exito" | "error">("cargando");
return <div>{usuario?.nombre ?? "Cargando..."}</div>;
}
useRef tipado
useRef tiene dos usos distintos en React, cada uno con su tipo correspondiente:
function ComponenteConRef() {
// Ref para un elemento del DOM (null como valor inicial)
const inputRef = React.useRef<HTMLInputElement>(null);
// Ref para un valor mutable (no DOM)
const contadorRef = React.useRef<number>(0);
const enfocarInput = () => {
// inputRef.current puede ser null (hay que verificarlo)
inputRef.current?.focus();
};
return <input ref={inputRef} />;
}
useReducer tipado
Para estados complejos, useReducer con TypeScript permite tipar exhaustivamente todas las acciones posibles:
interface EstadoCarrito {
items: { id: string; cantidad: number }[];
total: number;
}
type AccionCarrito =
| { tipo: "AGREGAR"; id: string }
| { tipo: "QUITAR"; id: string }
| { tipo: "VACIAR" };
function reductorCarrito(estado: EstadoCarrito, accion: AccionCarrito): EstadoCarrito {
switch (accion.tipo) {
case "AGREGAR":
return { ...estado, items: [...estado.items, { id: accion.id, cantidad: 1 }] };
case "QUITAR":
return { ...estado, items: estado.items.filter(i => i.id !== accion.id) };
case "VACIAR":
return { items: [], total: 0 };
// TypeScript garantiza exhaustividad
}
}
function Carrito() {
const [estado, despachar] = React.useReducer(reductorCarrito, { items: [], total: 0 });
return (
<button onClick={() => despachar({ tipo: "VACIAR" })}>
Vaciar carrito ({estado.items.length} items)
</button>
);
}
Contexto tipado
El contexto de React se tipa usando genéricos con createContext:
interface ContextoTema {
tema: "claro" | "oscuro";
alternarTema: () => void;
}
const ContextoTema = React.createContext<ContextoTema | null>(null);
// Hook personalizado que garantiza que el contexto existe
function useTema(): ContextoTema {
const contexto = React.useContext(ContextoTema);
if (!contexto) {
throw new Error("useTema debe usarse dentro de ProveedorTema");
}
return contexto;
}
function ProveedorTema({ children }: { children: React.ReactNode }) {
const [tema, setTema] = React.useState<"claro" | "oscuro">("claro");
return (
<ContextoTema.Provider value={{ tema, alternarTema: () => setTema(t => t === "claro" ? "oscuro" : "claro") }}>
{children}
</ContextoTema.Provider>
);
}
Hooks personalizados tipados
Los hooks personalizados en TypeScript pueden usar genéricos para mayor reutilización:
function useFetch<T>(url: string): {
datos: T | null;
cargando: boolean;
error: Error | null;
} {
const [datos, setDatos] = React.useState<T | null>(null);
const [cargando, setCargando] = React.useState(true);
const [error, setError] = React.useState<Error | null>(null);
React.useEffect(() => {
setCargando(true);
fetch(url)
.then(r => r.json())
.then((data: T) => {
setDatos(data);
setCargando(false);
})
.catch((err: Error) => {
setError(err);
setCargando(false);
});
}, [url]);
return { datos, cargando, error };
}
// Uso:
function ListaUsuarios() {
const { datos, cargando, error } = useFetch<Usuario[]>("/api/usuarios");
if (cargando) return <p>Cargando...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!datos) return null;
return <ul>{datos.map(u => <li key={u.id}>{u.nombre}</li>)}</ul>;
}
Componentes genéricos
Puedes crear componentes React completamente genéricos para mayor reutilización:
interface TablaProps<T> {
datos: T[];
columnas: {
clave: keyof T;
encabezado: string;
renderizar?: (valor: T[keyof T]) => React.ReactNode;
}[];
}
function Tabla<T extends { id: string | number }>({ datos, columnas }: TablaProps<T>) {
return (
<table>
<thead>
<tr>
{columnas.map(col => (
<th key={String(col.clave)}>{col.encabezado}</th>
))}
</tr>
</thead>
<tbody>
{datos.map(fila => (
<tr key={fila.id}>
{columnas.map(col => (
<td key={String(col.clave)}>
{col.renderizar
? col.renderizar(fila[col.clave])
: String(fila[col.clave])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Tipar componentes funcionales de React con interfaces de props
- Usar los tipos correctos para eventos del DOM en manejadores tipados
- Tipar el estado con useState y los efectos con useEffect
- Crear hooks personalizados tipados con genéricos
- Tipar el contexto de React con createContext para compartir estado global