TechLead
Lección 8 de 8
7 min de lectura
TypeScript

TypeScript con React

Tipa componentes, props, hooks y eventos para aplicaciones React seguras

Tipar componentes

TypeScript mejora significativamente el desarrollo con React al proporcionar seguridad de tipos en componentes, props y hooks. Definir interfaces para las props de tus componentes te permite detectar errores en tiempo de compilación y obtener autocompletado en tu editor. A continuación verás cómo tipar componentes funcionales con sus propiedades.

import React from 'react';

// Componente de función con interfaz de props
interface GreetingProps {
  name: string;
  age?: number;  // opcional
}

function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      Hello, {name}!
      {age && <span> You are {age} years old.</span>}
    </div>
  );
}

// Componente con función flecha
const Welcome: React.FC<GreetingProps> = ({ name, age }) => {
  return <h1>Welcome, {name}!</h1>;
};

// Componente con children
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Uso
<Card title="My Card">
  <p>Card content</p>
</Card>

Tipar props

Las props son el principal mecanismo de comunicación entre componentes en React. Con TypeScript puedes definir exactamente qué props acepta cada componente, cuáles son opcionales y qué tipos de valores se esperan. También puedes extender los atributos HTML nativos para crear componentes personalizados que hereden todas las propiedades estándar.

// Props con varios tipos
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  icon?: React.ReactNode;
  className?: string;
}

function Button({
  label,
  onClick,
  variant = 'primary',
  disabled = false,
  icon,
  className
}: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant} ${className || ''}`}
    >
      {icon && <span className="icon">{icon}</span>}
      {label}
    </button>
  );
}

// Props extendiendo atributos HTML
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...inputProps }: InputProps) {
  return (
    <div>
      <label>{label}</label>
      <input {...inputProps} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

// Patrón render props
interface DataFetcherProps<T> {
  url: string;
  render: (data: T | null, loading: boolean) => React.ReactNode;
}

Tipar hooks

Los hooks de React como useState, useRef, useEffect y useCallback se benefician enormemente del tipado con TypeScript. En muchos casos TypeScript puede inferir el tipo automáticamente, pero para estados que pueden ser null o contener tipos complejos es necesario especificar el tipo genérico explícitamente.

import { useState, useEffect, useRef, useCallback, useMemo } from 'react';

// useState con inferencia de tipo
const [count, setCount] = useState(0);  // inferido como number

// useState con tipo explícito
const [user, setUser] = useState<User | null>(null);

// useState con estado complejo
interface FormState {
  name: string;
  email: string;
  age: number;
}

const [form, setForm] = useState<FormState>({
  name: '',
  email: '',
  age: 0
});

// useRef
const inputRef = useRef<HTMLInputElement>(null);
const countRef = useRef<number>(0);

// useCallback con parámetros tipados
const handleClick = useCallback((id: number) => {
  console.log('Clicked:', id);
}, []);

// useMemo con tipo de retorno
const expensiveValue = useMemo<number>(() => {
  return someExpensiveCalculation();
}, [dependency]);

// useEffect con cleanup
useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    console.log(e.key);
  };

  window.addEventListener('keydown', handler);

  return () => {
    window.removeEventListener('keydown', handler);
  };
}, []);

Hooks personalizados

Crear hooks personalizados con TypeScript te permite encapsular lógica reutilizable con tipos seguros. Los genéricos son especialmente útiles aquí, ya que permiten que un mismo hook funcione con diferentes tipos de datos manteniendo la seguridad de tipos en todo momento.

// Hook personalizado con genéricos
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((val: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue] as const;
}

// Uso
const [name, setName] = useLocalStorage<string>('name', '');

// Hook de fetch
interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Uso
const { data: users, loading } = useFetch<User[]>('/api/users');

Manejo de eventos

React proporciona tipos específicos para cada tipo de evento del DOM. Usar estos tipos garantiza que accedes correctamente a las propiedades del evento y del elemento que lo disparó. Los tipos más comunes son MouseEvent, FormEvent, ChangeEvent y KeyboardEvent, cada uno parametrizado con el tipo de elemento HTML correspondiente.

// Eventos del mouse
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget);
};

// Eventos de formulario
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  // manejar envío del formulario
};

// Eventos de input
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setName(e.target.value);
};

// Eventos de select
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
  setOption(e.target.value);
};

// Eventos de teclado
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
  if (e.key === 'Enter') {
    handleSubmit();
  }
};

// Eventos de focus
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
  e.target.select();
};

// Eventos de drag
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
  e.preventDefault();
  const files = e.dataTransfer.files;
};

API de Contexto

La API de Contexto de React permite compartir datos entre componentes sin pasar props manualmente en cada nivel del árbol. Con TypeScript puedes definir el tipo exacto del contexto y crear un hook personalizado que garantice que el contexto se usa dentro de su proveedor correspondiente, lanzando un error claro si no es así.

import { createContext, useContext, useState, ReactNode } from 'react';

// Definir tipo de contexto
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// Crear contexto con valor por defecto
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Componente proveedor
interface ThemeProviderProps {
  children: ReactNode;
}

function ThemeProvider({ children }: ThemeProviderProps) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Hook personalizado para contexto
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
}

// Uso
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

Componentes genéricos

Los componentes genéricos son una de las características más poderosas de TypeScript con React. Te permiten crear componentes altamente reutilizables que funcionan con cualquier tipo de dato mientras mantienen la seguridad de tipos completa. Esto es especialmente útil para componentes como listas, tablas y selectores que necesitan manejar diferentes tipos de elementos.

// Componente de lista genérica
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// Uso
interface User {
  id: string;
  name: string;
}

const users: User[] = [
  { id: '1', name: 'Alice' },
  { id: '2', name: 'Bob' }
];

<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

Puntos clave

  • • Define interfaces para props de componentes
  • • Usa tipos genéricos para hooks como useState
  • • Tipa manejadores de eventos con tipos de React
  • • Crea hooks personalizados con tipos seguros
  • • Usa componentes genéricos para reutilización
  • • Tipa Context API con comprobaciones de null

Continuar Aprendiendo