TypeScript con React

Intermedio
TypeScript
TypeScript
Actualizado: 04/05/2026

Diagrama: tutorial-typescript-react

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 - Autor del tutorial

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