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.
¡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.
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 propnamedebe ser una cadena de texto.message?: string;: La propmessagees opcional (?) y, si se proporciona, debe ser una cadena de texto.age: number;: La propagedebe ser un número.isLoggedIn: boolean;: La propisLoggedIndebe ser un booleano.onButtonClick: (id: string) => void;: La proponButtonClickes una función que acepta un argumentoidde tipostringy 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;
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;
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 argumentoinitialStatees de tipobooleany tiene un valor por defecto.[boolean, () => void, (value: boolean) => void]: El valor de retorno es una tupla. Contiene el estado actual (boolean), una funcióntoggleque no acepta argumentos y no devuelve nada (() => void), y una funciónsetToggleque acepta unbooleany 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;
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;
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:
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.
- 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
- 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
},
};
- 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';.
// 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 buildtambié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 tipoAa una variable o propiedad que espera tipoB. 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 sernulloundefinedy 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).
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
- React Query: Gestión Eficiente de Datos Asíncronos y Caché en Reactintermediate20 min
- React Router DOM v6: Navegación Declarativa y Gestión de Rutas Avanzada 🚀intermediate20 min
- Optimización del Rendimiento en Aplicaciones React: Estrategias Avanzadas con Memoización y Virtualización de Listasadvanced18 min
- Gestión del Estado Global en React con Context API y useReducer: Una Guía Completaintermediate15 min
- React Hooks Personalizados: Creando Lógica Reutilizable y Abstraída 🛠️intermediate20 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!