TypeScript es una extensión de JavaScript que agrega tipado estático, lo cual es muy útil al trabajar con React. A continuación, te mostraré cómo configurar y usar TypeScript en un proyecto de React, junto con ejemplos prácticos.
Esta guía cubre los aspectos más importantes y actualizados de usar TypeScript con React, incluyendo:
npm create vite@latest my-react-ts-app -- --template react-ts
cd my-react-ts-app
npm install
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Un Componente Funcional Básico en React es una función de JavaScript que recibe props y retorna JSX, lo que permite renderizar UI sin usar clases.
import { FC } from 'react';
interface GreetingProps {
name: string;
age?: number; // Propiedad opcional
}
const Greeting: FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old</p>}
</div>
);
};
export default Greeting;
Un Componente con Eventos en React es un componente que incluye manejadores para interactuar con el usuario, como onClick, onChange o onSubmit. Estos eventos permiten ejecutar funciones cuando el usuario realiza acciones, como hacer clic o ingresar texto.
import { FC, ChangeEvent, FormEvent } from 'react';
interface LoginFormProps {
onSubmit: (username: string, password: string) => void;
}
const LoginForm: FC<LoginFormProps> = ({ onSubmit }) => {
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(
formData.get('username') as string,
formData.get('password') as string
);
};
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
type="text"
onChange={handleInputChange}
/>
<input
name="password"
type="password"
onChange={handleInputChange}
/>
<button type="submit">Login</button>
</form>
);
};
useState
El hook useState
en React permite agregar estado a los componentes funcionales.
Se usa para declarar una variable de estado y una función para actualizar su valor, permitiendo que el componente mantenga y cambie datos reactivos entre renderizados.
import { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
const loadUser = async (id: number) => {
setLoading(true);
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
setUser(data);
} finally {
setLoading(false);
}
};
return (
<div>
{loading && <p>Loading...</p>}
{user && (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)}
</div>
);
};
useEffect
El hook useEffect
en React permite ejecutar efectos secundarios en componentes funcionales, como hacer llamadas a APIs, actualizar el DOM o suscribirse a eventos.
Se ejecuta después de renderizar el componente, y puede configurarse para ejecutarse en cada render, solo una vez (como componentDidMount
) o cuando cambian dependencias específicas.
import { useEffect, useState } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
const PostList = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('https://api.example.com/posts');
const data = await response.json();
setPosts(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
}
};
fetchPosts();
}, []);
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
useReducer
El hook useReducer
en React es una alternativa a useState para manejar el estado complejo en componentes funcionales.
Utiliza un patrón similar a Redux, donde se define un reductor (función que recibe el estado actual y una acción) para decidir cómo actualizar el estado. Es útil para manejar múltiples valores de estado relacionados o cuando las actualizaciones de estado dependen de lógica más avanzada.
import { useReducer } from 'react';
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
type TodoAction =
| { type: 'ADD'; text: string }
| { type: 'TOGGLE'; id: number }
| { type: 'DELETE'; id: number };
interface TodoState {
todos: TodoItem[];
nextId: number;
}
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case 'ADD':
return {
...state,
todos: [...state.todos, { id: state.nextId, text: action.text, completed: false }],
nextId: state.nextId + 1
};
case 'TOGGLE':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
)
};
case 'DELETE':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.id)
};
}
};
const TodoList = () => {
const [state, dispatch] = useReducer(todoReducer, { todos: [], nextId: 1 });
return (
<div>
<button onClick={() => dispatch({ type: 'ADD', text: 'New Todo' })}>
Add Todo
</button>
<ul>
{state.todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch({ type: 'TOGGLE', id: todo.id })}
>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'DELETE', id: todo.id })}>
Delete
</button>
</li>
))}
</ul>
</div>
);
};
Los Custom Hooks
en React son funciones personalizadas que permiten reutilizar lógica de estado y efectos en varios componentes.
Son como los hooks integrados (useState
, useEffect
, etc.), pero creados por el usuario para encapsular funcionalidad específica, facilitando un código más limpio y modular.
import { useState, useEffect } from 'react';
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(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to fetch'));
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Uso del custom hook
interface Post {
id: number;
title: string;
}
const PostComponent = () => {
const { data, loading, error } = useFetch<Post[]>('/api/posts');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return null;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
Discriminated Unions
en TypeScript son una característica que permite crear tipos que pueden ser uno de varios tipos posibles, diferenciados por un campo común.
Esto es útil para definir props en componentes de React donde se necesita manejar diferentes tipos de datos de manera segura.
interface SuccessState {
status: 'success';
data: string[];
}
interface LoadingState {
status: 'loading';
}
interface ErrorState {
status: 'error';
error: string;
}
type State = SuccessState | LoadingState | ErrorState;
const StatusMessage: FC<State> = (state) => {
switch (state.status) {
case 'loading':
return <div>Loading...</div>;
case 'success':
return <div>Data: {state.data.join(', ')}</div>;
case 'error':
return <div>Error: {state.error}</div>;
}
};
Utility Types
en TypeScript son tipos predefinidos que facilitan la manipulación y transformación de otros tipos. Permiten crear nuevos tipos basados en tipos existentes mediante operaciones como la omisión, la inclusión o la modificación de propiedades. Algunos de los utility types más comunes son:
Partial<T>
: Crea un tipo donde todas las propiedades de T son opcionales.Required<T>
: Crea un tipo donde todas las propiedades de T son requeridas.Readonly<T>
: Crea un tipo donde todas las propiedades de T son de solo lectura.Record<K, T>
: Crea un tipo que representa un objeto con claves de tipo K y valores de tipo T.Pick<T, K>
: Crea un tipo con un subconjunto de propiedades de T que se especifican en K.Omit<T, K>
: Crea un tipo que omite las propiedades de T especificadas en K.Estos tipos son muy útiles para mantener un código más limpio y reducir la duplicación al trabajar con tipos complejos en aplicaciones TypeScript.
// Partial
interface Theme {
color: string;
backgroundColor: string;
fontSize: number;
}
const updateTheme = (theme: Theme, updates: Partial<Theme>) => {
return { ...theme, ...updates };
};
// Pick
interface User {
id: number;
name: string;
email: string;
password: string;
}
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit
type PublicUser = Omit<User, 'password'>;
// Record
type ValidStatus = 'draft' | 'published' | 'archived';
const postCounts: Record<ValidStatus, number> = {
draft: 5,
published: 10,
archived: 3
};
Typing children prop
en React se refiere a la práctica de definir el tipo de la propiedad children
en los componentes funcionales.
En React, children
representa cualquier contenido que se pasa a un componente, ya sea texto, elementos JSX, o incluso otros componentes.
import { ReactNode, FC } from 'react';
interface LayoutProps {
children: ReactNode;
sidebar?: ReactNode;
}
const Layout: FC<LayoutProps> = ({ children, sidebar }) => {
return (
<div className="layout">
{sidebar && <div className="sidebar">{sidebar}</div>}
<main>{children}</main>
</div>
);
};
Generic Components
en React son componentes que utilizan tipos genéricos para proporcionar flexibilidad y reutilización en el código.
Esto permite a los desarrolladores crear componentes que pueden funcionar con diferentes tipos de datos sin tener que duplicar el código.
interface SelectProps<T> {
items: T[];
value: T;
onChange: (value: T) => void;
getLabel: (item: T) => string;
}
function Select<T>({ items, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
value={items.indexOf(value)}
onChange={(e) => {
const index = parseInt(e.target.value);
onChange(items[index]);
}}
>
{items.map((item, index) => (
<option key={index} value={index}>
{getLabel(item)}
</option>
))}
</select>
);
}
// Uso
interface User {
id: number;
name: string;
}
const UserSelect = () => {
const users: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
return (
<Select<User>
items={users}
value={users[0]}
onChange={console.log}
getLabel={(user) => user.name}
/>
);
};
Type Guards
Type Guards
son una característica de TypeScript que permite comprobar y restringir los tipos de las variables en tiempo de ejecución.
Se utilizan para hacer que el código sea más seguro y predecible al garantizar que una variable sea de un tipo específico antes de realizar operaciones sobre ella.
Los Type Guards
permiten a TypeScript inferir el tipo de una variable dentro de un bloque de código, lo que ayuda a prevenir errores.
Esto se puede hacer a través de varias técnicas, como:
typeof
:Comprobar el tipo primitivo de una variable.
function esNumero(x: number | string) {
if (typeof x === 'number') {
console.log(x + 1); // Aquí TypeScript sabe que x es un número
} else {
console.log(x.length); // Aquí TypeScript sabe que x es un string
}
}
instanceof
:Comprobar si un objeto es una instancia de una clase específica.
class Perro {
ladrar() {
console.log("¡Guau!");
}
}
class Gato {
maullar() {
console.log("¡Miau!");
}
}
function hacerSonido(animal: Perro | Gato) {
if (animal instanceof Perro) {
animal.ladrar(); // Aquí TypeScript sabe que animal es un Perro
} else {
animal.maullar(); // Aquí TypeScript sabe que animal es un Gato
}
}
Crear funciones que devuelven un valor booleano y usan una declaración is
para ayudar a TypeScript a inferir el tipo.
interface Perro {
tipo: 'perro';
ladrar: () => void;
}
interface Gato {
tipo: 'gato';
maullar: () => void;
}
function esPerro(animal: Perro | Gato): animal is Perro {
return animal.tipo === 'perro';
}
function hacerSonido(animal: Perro | Gato) {
if (esPerro(animal)) {
animal.ladrar(); // TypeScript sabe que animal es un Perro
} else {
animal.maullar(); // TypeScript sabe que animal es un Gato
}
}
Este ejemplo en TypeScript muestra cómo utilizar discriminated unions para definir diferentes tipos de usuarios en una aplicación.
Aquí, se definen dos interfaces, AdminUser
y RegularUser
, que representan a los usuarios con distintos atributos.
La función isAdmin
actúa como un type guard, permitiendo verificar si un usuario es un administrador.
El componente UserInfo
muestra la información del usuario y, si es un administrador, también lista sus permisos.
Este enfoque mejora la claridad del código y la seguridad de tipos al manejar diferentes tipos de datos.
interface AdminUser {
type: 'admin';
name: string;
permissions: string[];
}
interface RegularUser {
type: 'regular';
name: string;
}
type User = AdminUser | RegularUser;
const isAdmin = (user: User): user is AdminUser => {
return user.type === 'admin';
};
const UserInfo: FC<{ user: User }> = ({ user }) => {
return (
<div>
<h2>{user.name}</h2>
{isAdmin(user) && (
<div>
Permissions: {user.permissions.join(', ')}
</div>
)}
</div>
);
};