tutoriales.com

React con TypeScript: Desarrollo de Aplicaciones Robustas y Escalables 🛡️

Este tutorial te guiará paso a paso en la integración de TypeScript en tus aplicaciones React. Descubrirás cómo aprovechar el tipado estático para escribir código más robusto, predecible y fácil de mantener, elevando la calidad de tus proyectos web.

Intermedio20 min de lectura6 views
Reportar error

¡Hola, desarrollador! 👋 ¿Estás listo para llevar tus habilidades en React al siguiente nivel? En el mundo del desarrollo web, la robustez y la mantenibilidad del código son cruciales, especialmente en aplicaciones de gran escala. Aquí es donde TypeScript entra en juego, ofreciendo una capa de seguridad y claridad al desarrollo con JavaScript.

Este tutorial se sumergirá en la poderosa combinación de React y TypeScript, mostrándote cómo el tipado estático puede transformar la forma en que construyes componentes, gestionas el estado y defines propiedades. Prepárate para escribir código más predecible, con menos errores y mucho más fácil de refactorizar.

¿Por qué React con TypeScript? 🤔

JavaScript es un lenguaje dinámico, lo cual ofrece gran flexibilidad, pero también puede ser una fuente de errores difíciles de depurar, especialmente a medida que las bases de código crecen. TypeScript, un superset de JavaScript, añade tipado estático, permitiéndonos definir los tipos de variables, funciones y objetos en tiempo de desarrollo.

Ventajas Clave de Usar TypeScript en React ✨

  • Detección temprana de errores: Muchos errores se capturan en tiempo de compilación, no en tiempo de ejecución, ahorrándote horas de depuración.
  • Mayor refactorización segura: Cambiar la estructura de tu código es menos arriesgado, ya que TypeScript te alertará sobre incompatibilidades de tipo.
  • Mejor auto-completado e IntelliSense: Los IDEs como VS Code pueden ofrecer sugerencias mucho más precisas, aumentando tu productividad.
  • Documentación viva: Los tipos actúan como una forma de documentación inherente, haciendo que el código sea más legible y comprensible para otros desarrolladores (¡o para tu yo futuro!).
  • Código más mantenible y escalable: Proyectos grandes con equipos extensos se benefician enormemente de la estructura y claridad que TypeScript proporciona.
💡 Consejo: Considera TypeScript no como una restricción, sino como una herramienta que te *guía* hacia un código más sólido y predecible.

Configuración de un Proyecto React con TypeScript 🛠️

Comencemos configurando un nuevo proyecto React que soporte TypeScript de forma nativa. La forma más sencilla es usar Create React App o Vite, ambos con plantillas preconfiguradas.

Opción 1: Create React App (CRA)

Si prefieres CRA, puedes inicializar un proyecto con TypeScript así:

npx create-react-app mi-app-ts --template typescript

Esto creará una nueva aplicación React con todas las dependencias necesarias para TypeScript ya configuradas. Podrás encontrar archivos .tsx y una configuración tsconfig.json lista para usar.

Opción 2: Vite (Recomendado para proyectos modernos) 🔥

Vite es un bundler más rápido y moderno, ideal para iniciar nuevos proyectos. Para crear una aplicación React con TypeScript usando Vite:

npm create vite@latest mi-app-ts -- --template react-ts

Luego, navega al directorio del proyecto e instala las dependencias:

cd mi-app-ts
npm install
npm run dev

¡Felicidades! 🎉 Ya tienes tu entorno de desarrollo listo para empezar a escribir código React con TypeScript.


Fundamentos de TypeScript en React: Tipos Comunes y Definiciones 📖

Ahora que tenemos nuestro entorno listo, es hora de sumergirnos en cómo aplicar TypeScript a los componentes de React. El corazón de esto es definir los tipos para las props y el estado de tus componentes.

Tipando Props de Componentes Funcionales (FC) 🛡️

En React, los componentes funcionales son la norma. Para tipar sus props, usamos interfaces o tipos.

// src/components/Greeting.tsx

import React from 'react';

// 1. Define una interfaz para las props
interface GreetingProps {
  name: string;
  message?: string; // El signo '?' indica que es opcional
  age: number;
  isLoggedIn: boolean;
  onButtonClick: (id: string) => void; // Función que recibe un string y no devuelve nada
}

// 2. Asigna la interfaz a las props del componente
const Greeting: React.FC<GreetingProps> = ({ name, message, age, isLoggedIn, onButtonClick }) => {
  const displayMessage = message || '¡Bienvenido!';

  return (
    <div>
      {isLoggedIn ? (
        <p>Hola, {name}! ({age} años) {displayMessage}</p>
      ) : (
        <p>Por favor, inicia sesión.</p>
      )}
      <button onClick={() => onButtonClick('user-123')}>Hacer algo</button>
    </div>
  );
};

export default Greeting;

Explicación de Tipos en GreetingProps:

  • name: string;: La prop name debe ser una cadena de texto.
  • message?: string;: La prop message es opcional (?) y, si se proporciona, debe ser una cadena de texto.
  • age: number;: La prop age debe ser un número.
  • isLoggedIn: boolean;: La prop isLoggedIn debe ser un booleano.
  • onButtonClick: (id: string) => void;: La prop onButtonClick es una función que acepta un argumento id de tipo string y no devuelve ningún valor (void).

Cómo usar el componente:

// src/App.tsx

import React from 'react';
import Greeting from './components/Greeting';

const App: React.FC = () => {
  const handleButtonClick = (userId: string) => {
    console.log(`Botón presionado por el usuario: ${userId}`);
    // Aquí podrías realizar una acción con el userId
  };

  return (
    <div>
      <h1>Mi Aplicación con TypeScript</h1>
      <Greeting 
        name="Alicia" 
        age={30} 
        isLoggedIn={true} 
        onButtonClick={handleButtonClick} 
      />
      <Greeting 
        name="Bob" 
        age={25} 
        isLoggedIn={false} 
        onButtonClick={handleButtonClick} 
      />
       <Greeting 
        name="Carlos" 
        age={28} 
        isLoggedIn={true} 
        message="¡Hola de nuevo!" 
        onButtonClick={handleButtonClick} 
      />
    </div>
  );
};

export default App;
📌 Nota: Cuando usas `React.FC`, TypeScript infiere el tipo de `children` como `ReactNode`. Si tu componente no debe recibir *children*, es mejor no usar `React.FC` y tipar las props directamente: `const MyComponent = ({ prop1 }: MyProps) => { /* ... */ }`.

Tipando el Estado con useState y useReducer 🎯

El tipado del estado es fundamental para mantener la coherencia de los datos en tu aplicación.

useState

TypeScript generalmente puede inferir el tipo del estado inicial, pero es una buena práctica ser explícito, especialmente si el estado puede ser null o undefined inicialmente, o si el tipo es complejo.

// src/components/Counter.tsx

import React, { useState } from 'react';

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

const Counter: React.FC = () => {
  // Inferencia simple: 'count' es number
  const [count, setCount] = useState(0);

  // Estado con posible 'null' o 'undefined'
  const [user, setUser] = useState<User | null>(null);

  // Estado de array de objetos
  const [items, setItems] = useState<string[]>([]);

  const increment = () => setCount(prevCount => prevCount + 1);
  const decrement = () => setCount(prevCount => prevCount - 1);

  const fetchUser = () => {
    // Simula una llamada API
    setTimeout(() => {
      setUser({ id: 'u1', name: 'Juan Pérez', email: 'juan@example.com' });
    }, 1000);
  };

  const addItem = (item: string) => {
    setItems(prevItems => [...prevItems, item]);
  };

  return (
    <div>
      <h2>Contador: {count}</h2>
      <button onClick={increment}>Incrementar</button>
      <button onClick={decrement}>Decrementar</button>

      <h2>Usuario:</h2>
      {user ? (
        <p>ID: {user.id}, Nombre: {user.name}, Email: {user.email}</p>
      ) : (
        <button onClick={fetchUser}>Cargar Usuario</button>
      )}

      <h2>Items:</h2>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
      <button onClick={() => addItem(`Item ${items.length + 1}`)}>Añadir Item</button>
    </div>
  );
};

export default Counter;

useReducer

Para useReducer, necesitarás definir los tipos para el estado y para las acciones (uniones discriminadas son muy útiles aquí).

// src/components/TodoApp.tsx

import React, { useReducer } from 'react';

// 1. Definir el tipo para un 'Todo' individual
interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

// 2. Definir el tipo para el estado completo de la aplicación
interface TodoState {
  todos: Todo[];
}

// 3. Definir los tipos para las acciones
type TodoAction =
  | { type: 'ADD_TODO'; payload: string } // payload es el texto del todo
  | { type: 'TOGGLE_TODO'; payload: string } // payload es el id del todo
  | { type: 'REMOVE_TODO'; payload: string }; // payload es el id del todo

// 4. Definir el estado inicial
const initialState: TodoState = { 
  todos: [] 
};

// 5. El reducer: una función que toma el estado actual y una acción, y devuelve un nuevo estado
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now().toString(), text: action.payload, completed: false }],
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        ),
      };
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };
    default:
      return state;
  }
};

const TodoApp: React.FC = () => {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [newTodoText, setNewTodoText] = useState('');

  const handleAddTodo = () => {
    if (newTodoText.trim()) {
      dispatch({ type: 'ADD_TODO', payload: newTodoText });
      setNewTodoText('');
    }
  };

  return (
    <div>
      <h2>Lista de Tareas</h2>
      <div>
        <input
          type="text"
          value={newTodoText}
          onChange={(e) => setNewTodoText(e.target.value)}
          placeholder="Añadir nueva tarea..."
        />
        <button onClick={handleAddTodo}>Añadir</button>
      </div>
      <ul>
        {state.todos.map((todo) => (
          <li 
            key={todo.id} 
            style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
          >
            <span onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: 'REMOVE_TODO', payload: todo.id })}>X</button>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;

Tipado de Eventos y Manejadores en React con TypeScript 🖱️

Cuando trabajamos con eventos en React, TypeScript nos ayuda a garantizar que los manejadores de eventos reciban el objeto de evento correcto y que accedamos a sus propiedades de forma segura.

Tipos de Eventos Comunes ✅

React expone sus propios tipos de evento sintéticos, que son envolturas alrededor de los eventos nativos del navegador.

Aquí tienes algunos de los más comunes:

  • React.MouseEvent<HTMLButtonElement>: Para eventos de clic en botones.
  • React.ChangeEvent<HTMLInputElement>: Para eventos de cambio en inputs.
  • React.FormEvent<HTMLFormElement>: Para eventos de submit en formularios.
  • React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>: Para eventos de teclado en inputs o textareas.
// src/components/EventHandling.tsx

import React, { useState } from 'react';

const EventHandling: React.FC = () => {
  const [inputValue, setInputValue] = useState('');
  const [clickCount, setClickCount] = useState(0);

  // Manejador para evento de cambio en input
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };

  // Manejador para evento de clic en botón
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    setClickCount(prevCount => prevCount + 1);
    console.log(`Coordenadas del clic: X=${e.clientX}, Y=${e.clientY}`);
  };

  // Manejador para evento de submit de formulario
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // Previene la recarga de la página
    console.log(`Formulario enviado con valor: ${inputValue}`);
    alert(`Enviado: ${inputValue}`);
  };

  // Manejador para evento de teclado (ej. 'Enter')
  const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      console.log('Tecla Enter presionada.');
      handleSubmit(e as unknown as React.FormEvent<HTMLFormElement>); // Cast necesario para llamar handleSubmit
    }
  };

  return (
    <div>
      <h2>Manejo de Eventos con TypeScript</h2>

      <form onSubmit={handleSubmit}>
        <label>
          Input de texto:
          <input 
            type="text" 
            value={inputValue} 
            onChange={handleChange} 
            onKeyPress={handleKeyPress} 
            placeholder="Escribe algo..."
          />
        </label>
        <button type="submit">Enviar Formulario</button>
      </form>

      <p>Valor actual del input: <mark>{inputValue || 'Vacío'}</mark></p>

      <button onClick={handleClick}>
        Clic aquí ({clickCount} veces)
      </button>

      <p>Este componente demuestra cómo tipar diferentes tipos de eventos.</p>
    </div>
  );
};

export default EventHandling;
⚠️ Advertencia: Cuando hagas un 'cast' como `e as unknown as React.FormEvent`, hazlo con precaución y solo si estás seguro de la compatibilidad de los tipos, como en el ejemplo `onKeyPress` para reutilizar `handleSubmit`. Idealmente, los manejadores deben recibir el tipo de evento exacto.

Hooks Personalizados con TypeScript 🎣

Los custom hooks son una forma poderosa de reutilizar lógica con estado en React. TypeScript los hace aún más robustos al garantizar que los valores que devuelven y los argumentos que aceptan estén correctamente tipados.

Vamos a crear un hook personalizado simple para gestionar el estado de un toggle.

// src/hooks/useToggle.ts

import { useState, useCallback } from 'react';

/**
 * @function useToggle
 * @description Hook personalizado para gestionar un estado booleano de encendido/apagado.
 * @param {boolean} initialState - El estado inicial del toggle (por defecto, false).
 * @returns {[boolean, () => void, (value: boolean) => void]}
 *          Retorna el valor actual, una función para alternar y una función para establecer un valor específico.
 */
function useToggle(initialState: boolean = false): [boolean, () => void, (value: boolean) => void] {
  const [state, setState] = useState<boolean>(initialState);

  // Función para alternar el estado
  const toggle = useCallback(() => setState(prevState => !prevState), []);

  // Función para establecer el estado a un valor específico
  const setToggle = useCallback((value: boolean) => setState(value), []);

  return [state, toggle, setToggle];
}

export default useToggle;

Explicación del tipado de useToggle:

  • initialState: boolean = false: El argumento initialState es de tipo boolean y tiene un valor por defecto.
  • [boolean, () => void, (value: boolean) => void]: El valor de retorno es una tupla. Contiene el estado actual (boolean), una función toggle que no acepta argumentos y no devuelve nada (() => void), y una función setToggle que acepta un boolean y no devuelve nada ((value: boolean) => void).

Uso del Hook Personalizado:

// src/components/ToggleSwitch.tsx

import React from 'react';
import useToggle from '../hooks/useToggle'; // Importa el hook personalizado

const ToggleSwitch: React.FC = () => {
  // Usa el hook personalizado
  const [isOn, toggle, setIsOn] = useToggle(false);

  return (
    <div>
      <h2>Control de Interruptor</h2>
      <p>El interruptor está: {isOn ? <span class="badge green">ENCENDIDO</span> : <span class="badge red">APAGADO</span>}</p>
      <button onClick={toggle}>Alternar</button>
      <button onClick={() => setIsOn(true)}>Encender</button>
      <button onClick={() => setIsOn(false)}>Apagar</button>

      <div class="progress-bar" style={{ width: '200px', height: '20px', marginTop: '15px' }}>
        <div 
          class="progress-fill"
          style={{
            width: isOn ? '100%' : '0%',
            background: isOn ? '#4CAF50' : '#f44336',
            transition: 'width 0.3s ease-in-out'
          }}
        >
          {isOn ? '100% On' : '0% Off'}
        </div>
      </div>
    </div>
  );
};

export default ToggleSwitch;

Componentes de Clase con TypeScript (Contexto y Lifecycles) 🏛️

Aunque los componentes funcionales con Hooks son el enfoque preferido en React moderno, todavía es importante saber cómo tipar los componentes de clase, especialmente si trabajas con bases de código legadas o necesitas características específicas de estos.

Tipado de props y state en Componentes de Clase

Los componentes de clase extienden React.Component<Props, State>. Aquí, Props y State son interfaces que defines para tipar las propiedades y el estado respectivamente.

// src/components/ClassCounter.tsx

import React, { Component } from 'react';

interface ClassCounterProps {
  initialValue?: number;
  title: string;
}

interface ClassCounterState {
  count: number;
}

class ClassCounter extends Component<ClassCounterProps, ClassCounterState> {
  // Establece los tipos por defecto para props opcionales
  static defaultProps = {
    initialValue: 0
  };

  constructor(props: ClassCounterProps) {
    super(props);
    this.state = {
      count: props.initialValue! // Usamos ! porque sabemos que defaultProps asegura un valor
    };
  }

  // Método para incrementar el contador
  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  };

  // Método para decrementar el contador
  decrement = () => {
    this.setState(prevState => ({
      count: prevState.count - 1
    }));
  };

  render() {
    const { title } = this.props;
    const { count } = this.state;

    return (
      <div>
        <h3>{title}</h3>
        <p>Contador de Clase: {count}</p>
        <button onClick={this.increment}>Incrementar</button>
        <button onClick={this.decrement}>Decrementar</button>
      </div>
    );
  }
}

export default ClassCounter;
¿Por qué `initialValue!`? En el constructor de `ClassCounter`, usamos `props.initialValue!` (un *non-null assertion operator*). Esto le dice a TypeScript que, a pesar de que `initialValue` está marcado como opcional en `ClassCounterProps`, en este punto específico del código (después de que `defaultProps` ha sido aplicado), TypeScript puede asumir que `initialValue` tendrá un valor definido. Es una forma de manejar propiedades opcionales con valores por defecto en componentes de clase.

Context API con Componentes de Clase (y TypeScript)

El Context API también se puede usar con componentes de clase, y TypeScript asegura que los valores del contexto sean consistentes.

// src/context/ThemeContext.tsx

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

// 1. Definir la interfaz para el valor del contexto
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// 2. Crear el contexto con un valor por defecto (y un cast para el tipado)
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// 3. Crear el proveedor del contexto
interface ThemeProviderProps {
  children: React.ReactNode;
}

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

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

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

// 4. Crear un hook personalizado para consumir el contexto (opcional pero recomendado)
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme debe usarse dentro de un ThemeProvider');
  }
  return context;
};

// src/components/ThemeSwitcherClass.tsx

import React, { Component } from 'react';
import { ThemeContextType, ThemeContext } from '../context/ThemeContext'; // Importar ThemeContext y su tipo

interface ThemeSwitcherClassState {
  // No hay estado interno para este componente, pero la interfaz es necesaria
}

class ThemeSwitcherClass extends Component<{}, ThemeSwitcherClassState> {
  // Definir el tipo del contexto que el componente va a consumir
  static contextType = ThemeContext;
  declare context: React.ContextType<typeof ThemeContext>;

  render() {
    // Asegurarse de que el contexto no sea 'undefined' antes de usarlo
    if (this.context === undefined) {
      return <div>Error: ThemeContext no proporcionado.</div>;
    }

    const { theme, toggleTheme } = this.context;

    return (
      <div style={{ padding: '20px', background: theme === 'light' ? '#eee' : '#333', color: theme === 'light' ? '#333' : '#eee' }}>
        <h4>Componente de Clase con Contexto</h4>
        <p>Tema actual: <span className={theme === 'light' ? 'badge blue' : 'badge purple'}>{theme.toUpperCase()}</span></p>
        <button onClick={toggleTheme}>Alternar Tema</button>
      </div>
    );
  }
}

export default ThemeSwitcherClass;

Uso en App.tsx:

// src/App.tsx (fragmento)

import React from 'react';
import Greeting from './components/Greeting';
import Counter from './components/Counter';
import TodoApp from './components/TodoApp';
import EventHandling from './components/EventHandling';
import ToggleSwitch from './components/ToggleSwitch';
import ClassCounter from './components/ClassCounter';
import ThemeSwitcherClass from './components/ThemeSwitcherClass';
import { ThemeProvider } from './context/ThemeContext'; // Importar el ThemeProvider

const App: React.FC = () => {
  // ... handleButtonClick, etc.

  return (
    <ThemeProvider> {/* Envuelve tu aplicación con el proveedor de tema */}
      <div>
        <h1>Mi Aplicación React con TypeScript</h1>

        <ThemeSwitcherClass />
        <hr />
        
        <Greeting 
          name="Alicia" 
          age={30} 
          isLoggedIn={true} 
          onButtonClick={handleButtonClick} 
        />
        <hr />

        <Counter />
        <hr />

        <TodoApp />
        <hr />

        <EventHandling />
        <hr />

        <ToggleSwitch />
        <hr />

        <ClassCounter title="Contador de Clase Principal" initialValue={10} />
        <ClassCounter title="Otro Contador de Clase" /> {/* Usa el initialValue por defecto */}
        
      </div>
    </ThemeProvider>
  );
};

export default App;
🔥 Importante: Para que TypeScript reconozca el contexto en un componente de clase, necesitas las propiedades `static contextType = MyContext;` y `declare context: React.ContextType;`. El `declare` es crucial para que TypeScript sepa qué tipo esperar en `this.context`.

Patrones Avanzados y Buenas Prácticas con TypeScript y React 🚀

Una vez que dominas los fundamentos, puedes explorar patrones más avanzados que aprovechan al máximo el tipado estático.

Generics en Componentes y Hooks 🧩

Los generics permiten escribir componentes y hooks que funcionan con varios tipos de datos sin perder el tipado estático. Esto es ideal para componentes reutilizables como tablas, listas o selectores.

Vamos a crear un componente de lista genérico.

// src/components/GenericList.tsx

import React from 'react';

interface GenericListProps<T> {
  items: T[]; // El array de items es del tipo genérico T
  renderItem: (item: T) => React.ReactNode; // La función renderItem recibe un item de tipo T
  getKey: (item: T) => string | number; // Función para obtener una clave única de un item de tipo T
}

/**
 * Componente genérico para renderizar una lista de cualquier tipo de datos.
 */
function GenericList<T>({
  items,
  renderItem,
  getKey,
}: GenericListProps<T>): React.ReactElement {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

export default GenericList;

Uso del Componente Genérico:

// src/App.tsx (fragmento)

import React from 'react';
// ... otros imports
import GenericList from './components/GenericList';

interface Product {
  id: string;
  name: string;
  price: number;
}

interface UserData {
  userId: number;
  username: string;
  isActive: boolean;
}

const App: React.FC = () => {
  // ... otros estados y manejadores

  const products: Product[] = [
    { id: 'p1', name: 'Laptop', price: 1200 },
    { id: 'p2', name: 'Teclado Mecánico', price: 150 },
    { id: 'p3', name: 'Monitor UltraWide', price: 400 },
  ];

  const users: UserData[] = [
    { userId: 1, username: 'alice_smith', isActive: true },
    { userId: 2, username: 'bob_johnson', isActive: false },
    { userId: 3, username: 'charlie_brown', isActive: true },
  ];

  return (
    <ThemeProvider>
      <div>
        {/* ... otros componentes ... */}

        <h2>Lista de Productos</h2>
        <GenericList
          items={products}
          getKey={(product) => product.id}
          renderItem={(product) => (
            <span>
              **{product.name}** - ${product.price}
            </span>
          )}
        />
        <hr />

        <h2>Lista de Usuarios</h2>
        <GenericList
          items={users}
          getKey={(user) => user.userId}
          renderItem={(user) => (
            <span style={{ color: user.isActive ? 'green' : 'red' }}>
              @{user.username} ({user.isActive ? 'Activo' : 'Inactivo'})
            </span>
          )}
        />
      </div>
    </ThemeProvider>
  );
};

export default App;

Uniones Discriminadas para Reducers Complejos (Revisitado) 🚥

Las uniones discriminadas (o discriminated unions) son un patrón muy potente en TypeScript para manejar tipos que tienen una propiedad común pero cuyos otros campos varían según el valor de esa propiedad. Son especialmente útiles en useReducer.

Ya lo vimos brevemente en el todoReducer, donde el type de la acción era la propiedad discriminante. Veamos otro ejemplo más complejo.

// src/reducers/ComplexFormReducer.ts

// 1. Definir el estado del formulario
interface FormState {
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  occupation: string;
  isSubmitted: boolean;
  errors: Record<string, string | undefined>;
}

// 2. Definir los tipos de acciones usando uniones discriminadas
type FormAction =
  | { type: 'CHANGE_FIELD'; payload: { field: keyof Omit<FormState, 'isSubmitted' | 'errors'>; value: string | number } }
  | { type: 'SUBMIT_FORM' }
  | { type: 'RESET_FORM' }
  | { type: 'SET_ERROR'; payload: { field: keyof FormState; message: string | undefined } };

// 3. Estado inicial
const initialFormState: FormState = {
  firstName: '',
  lastName: '',
  email: '',
  age: 0,
  occupation: '',
  isSubmitted: false,
  errors: {},
};

// 4. El reducer
const formReducer = (state: FormState, action: FormAction): FormState => {
  switch (action.type) {
    case 'CHANGE_FIELD':
      return {
        ...state,
        [action.payload.field]: action.payload.value,
        errors: { ...state.errors, [action.payload.field]: undefined }, // Limpiar error al cambiar campo
      };
    case 'SUBMIT_FORM':
      // Aquí podrías añadir lógica de validación más robusta
      const newErrors: Record<string, string | undefined> = {};
      if (!state.firstName) newErrors.firstName = 'El nombre es requerido';
      if (!state.email.includes('@')) newErrors.email = 'Email inválido';

      if (Object.keys(newErrors).length > 0) {
        return { ...state, errors: newErrors };
      }

      console.log('Formulario enviado:', state);
      return { ...state, isSubmitted: true, errors: {} };
    case 'RESET_FORM':
      return { ...initialFormState };
    case 'SET_ERROR':
      return {
        ...state,
        errors: { ...state.errors, [action.payload.field]: action.payload.message },
      };
    default:
      return state;
  }
};

export { formReducer, initialFormState };

Uso en un componente de formulario:

// src/components/ComplexForm.tsx

import React, { useReducer, useState } from 'react';
import { formReducer, initialFormState } from '../reducers/ComplexFormReducer';

const ComplexForm: React.FC = () => {
  const [state, dispatch] = useReducer(formReducer, initialFormState);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value, type } = e.target;
    dispatch({ 
      type: 'CHANGE_FIELD', 
      payload: { 
        field: name as keyof typeof initialFormState, 
        value: type === 'number' ? parseInt(value) : value 
      } 
    });
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    dispatch({ type: 'SUBMIT_FORM' });
  };

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '20px 0' }}>
      <h3>Formulario Complejo con `useReducer`</h3>
      <form onSubmit={handleSubmit}>
        <div>
          <label>Nombre:</label>
          <input 
            type="text" 
            name="firstName" 
            value={state.firstName} 
            onChange={handleChange}
          />
          {state.errors.firstName && <p style={{ color: 'red' }}>{state.errors.firstName}</p>}
        </div>
        <div>
          <label>Apellido:</label>
          <input 
            type="text" 
            name="lastName" 
            value={state.lastName} 
            onChange={handleChange}
          />
        </div>
        <div>
          <label>Email:</label>
          <input 
            type="email" 
            name="email" 
            value={state.email} 
            onChange={handleChange}
          />
          {state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
        </div>
        <div>
          <label>Edad:</label>
          <input 
            type="number" 
            name="age" 
            value={state.age} 
            onChange={handleChange}
          />
        </div>
        <div>
          <label>Ocupación:</label>
          <select name="occupation" value={state.occupation} onChange={handleChange}>
            <option value="">Selecciona...</option>
            <option value="dev">Desarrollador</option>
            <option value="designer">Diseñador</option>
            <option value="other">Otro</option>
          </select>
        </div>
        <button type="submit" style={{ marginTop: '10px' }}>Enviar</button>
        <button type="button" onClick={() => dispatch({ type: 'RESET_FORM' })} style={{ marginTop: '10px', marginLeft: '10px' }}>Resetear</button>
      </form>
      {state.isSubmitted && !Object.keys(state.errors).length && <p style={{ color: 'green', fontWeight: 'bold' }}>¡Formulario enviado con éxito!</p>}
    </div>
  );
};

export default ComplexForm;
Inicio Definir Interfaces (Props, State) Componente TSX (FC o Class) Usar Tipos en Props/Estado Tipar Eventos/Hooks Compilación con TS (Errores detectados aquí) Renderizado en Navegador

tsconfig.json y Opciones Importantes ⚙️

El archivo tsconfig.json es el corazón de tu configuración TypeScript. Aquí algunas opciones clave para proyectos React:

| Opción | Descripción | Valor Típico | Impacto | |---|---|---|---| | --- | --- | --- | --- | | `target` | Versión de JavaScript de salida | `es2020` o `esnext` | Compatibilidad del navegador. | | `module` | Sistema de módulos a usar | `esnext` o `commonjs` | Cómo se generan los imports/exports. | | --- | --- | --- | --- | | `jsx` | Cómo TypeScript maneja el JSX | `react-jsx` (para React 17+) | Define cómo JSX se transpila. | | `strict` | Habilita un conjunto de controles de tipo estrictos | `true` | **MUY RECOMENDADO** para un código robusto. | | --- | --- | --- | --- | | `esModuleInterop` | Habilita la interoperabilidad con módulos CommonJS | `true` | Facilita el trabajo con módulos JS antiguos. | | `forceConsistentCasingInFileNames` | Asegura consistencia en mayúsculas/minúsculas de nombres de archivo | `true` | Previene errores en sistemas de archivos sensibles a mayúsculas. | | --- | --- | --- | --- | | `skipLibCheck` | Omite la verificación de tipos de los archivos de declaración de bibliotecas | `true` | Acelera la compilación, pero oculta posibles errores en las definiciones de terceros. | | `lib` | Bibliotecas de declaración de tipos para incluir | `['dom', 'dom.iterable', 'esnext']` | Proporciona tipado para APIs del navegador y JS moderno. | | --- | --- | --- | --- | | `include` | Archivos a incluir en el compilador | `['src']` | Dónde buscar tus archivos fuente. | | `exclude` | Archivos a excluir del compilador | `['node_modules']` | Archivos que no deben ser compilados. |
💡 Consejo: Empieza con `strict: true`. Te ayudará a escribir código de mayor calidad desde el principio. Aunque al principio puede parecer molesto, los beneficios a largo plazo son enormes.

Herramientas y Ecosistema 🌍

El ecosistema de TypeScript es vasto y hay varias herramientas que mejoran la experiencia de desarrollo con React.

ESLint y Prettier 💅

La combinación de ESLint (para la calidad del código) y Prettier (para el formato del código) es esencial. Para TypeScript, necesitarás algunas configuraciones y plugins específicos.

  1. Instalar dependencias (si no las tienes):
npm install --save-dev eslint prettier eslint-plugin-react @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier
  1. Configurar .eslintrc.js (ejemplo):
// .eslintrc.js
module.exports = {
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'plugin:prettier/recommended' 
],
settings: {
react: {
version: 'detect',
},
},
env: {
browser: true,
node: true,
es6: true,
},
plugins: [
'react',
'@typescript-eslint',
'prettier'
],
rules: {
// Puedes añadir o sobrescribir reglas aquí
'prettier/prettier': 'error',
'react/react-in-jsx-scope': 'off', // No es necesario en React 17+
'@typescript-eslint/explicit-module-boundary-types': 'off', // Puedes habilitarlo si quieres ser muy estricto con los tipos de retorno de funciones exportadas
},
};
  1. Configurar .prettierrc.js (ejemplo):
// .prettierrc.js
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 100,
tabWidth: 2,
};

Alias de Rutas con baseUrl y paths 📁

Para evitar importaciones relativas largas y tediosas (../../../components/Button), puedes configurar alias de rutas en tu tsconfig.json.

// tsconfig.json
{
  "compilerOptions": {
    // ... otras opciones
    "baseUrl": "src", // Directorio base para resolver módulos no relativos
    "paths": {
      "@components/*": ["components/*"], // Alias para src/components
      "@hooks/*": ["hooks/*"],
      "@utils/*": ["utils/*"],
      "@context/*": ["context/*"]
    }
  },
  // ...
}

Ahora, en lugar de import Button from '../../components/Button';, puedes usar import Button from '@components/Button';.

⚠️ Advertencia: Si estás usando `Vite`, también necesitarás configurar alias en `vite.config.ts` para que el *bundler* los reconozca durante el desarrollo y la construcción. Para `Create React App`, los alias de `tsconfig.json` suelen ser suficientes.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@components': path.resolve(__dirname, './src/components'),
      '@hooks': path.resolve(__dirname, './src/hooks'),
      '@utils': path.resolve(__dirname, './src/utils'),
      '@context': path.resolve(__dirname, './src/context'),
    },
  },
});

Despliegue y Consideraciones Finales 🚀

Compilar una aplicación React con TypeScript es un proceso estándar. Tu bundler (Vite, Webpack, etc.) usará tsc internamente para transcompilar el código TypeScript a JavaScript antes de empaquetarlo.

  • Vite: Simplemente ejecuta npm run build. Vite se encargará de la transpilación y optimización.
  • Create React App: npm run build también manejará todo, gracias a las configuraciones preestablecidas.

Errores Comunes y Soluciones Rápidas 🐞

  • Property 'X' does not exist on type 'Y': El error más común. Significa que estás intentando acceder a una propiedad que TypeScript no cree que exista en ese tipo. Revisa tu interfaz/tipo y asegúrate de que la propiedad esté definida o considera usar el operador ? para propiedades opcionales o el operador ! si estás seguro de que el valor no será nulo/indefinido en tiempo de ejecución.
  • Type 'A' is not assignable to type 'B': Indica una incompatibilidad de tipos al intentar asignar un valor de tipo A a una variable o propiedad que espera tipo B. Revisa las definiciones de tipo y el valor que estás pasando.
  • Object is possibly 'null' or 'undefined': Ocurre en modo estricto cuando TypeScript detecta que una variable podría ser null o undefined y estás intentando usarla como si siempre tuviera un valor. Puedes usar comprobaciones condicionales (if (value) { ... }), el operador de encadenamiento opcional (value?.property), o el operador de aserción no nulo (value!) si estás absolutamente seguro.
  • Problemas con bibliotecas de terceros: Algunas bibliotecas no tienen tipos incorporados. Puedes necesitar instalar los tipos de @types/nombre-de-la-biblioteca (ej. npm install --save-dev @types/react-router-dom). Si no existen, puedes crear tus propios archivos de declaración (.d.ts).
💡 Consejo: Usa el comando `tsc --noEmit` en tu terminal para verificar errores de tipo sin generar archivos JavaScript. Esto es útil para pre-validaciones en tu flujo de trabajo o en CI/CD.
Recursos Adicionales para Profundizar
  • **Documentación oficial de TypeScript:** [https://www.typescriptlang.org/docs/](https://www.typescriptlang.org/docs/)
  • **React TypeScript Cheatsheet:** [https://github.com/typescript-cheatsheets/react](https://github.com/typescript-cheatsheets/react) (Un recurso excelente con muchos ejemplos y patrones)
  • **Documentación de React:** [https://react.dev/](https://react.dev/)

Conclusión ✨

Integrar TypeScript en tus proyectos React es una inversión que rinde dividendos en términos de robustez, mantenibilidad y escalabilidad. Si bien puede haber una curva de aprendizaje inicial, los beneficios de tener un código más predecible, con menos errores y mejor documentado son inmensos. Te empodera para construir aplicaciones más grandes y complejas con confianza y facilita la colaboración en equipo.

¡Anímate a adoptar TypeScript y transforma tu experiencia de desarrollo en React!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!