TypeScript Proyecto práctico integral
Este tutorial utiliza un caso de proyecto completo para aplicar de manera integral las diversas características de TypeScript.
Desde la configuración del proyecto hasta el desarrollo real, se muestra completamente la aplicación de TypeScript en proyectos reales.
Diagrama SVG: Arquitectura del proyecto
¿Por qué se necesita un proyecto práctico integral?
Después de aprender la sintaxis de TypeScript, es necesario consolidar los conocimientos a través de un proyecto real.
Este tutorial muestra un sistema completo de gestión de tareas, que cubre frontend, backend y definiciones de tipos.
Con este proyecto, se pueden dominar las mejores prácticas de TypeScript en el desarrollo real.
Objetivos del proyecto:Crear un sistema de gestión de tareas que incluya funciones de creación, consulta, actualización y eliminación de tareas.
Estructura del proyecto
Organizar la estructura del proyecto con estilo Monorepo.
Estructura de directorios
├── src/
│ ├── types/# Definiciones de tipos
│ │ ├── task.ts# Tipos de tarea
│ │ ├── api.ts# Tipos de API
│ │ └── index.ts# Exportación de tipos
│ │
│ ├── services/# Capa de servicio
│ │ ├── taskService.ts# Servicio de tareas
│ │ └── index.ts
│ │
│ ├── components/# Componentes React
│ │ ├── TaskList.tsx# Lista de tareas
│ │ ├── TaskItem.tsx# Elemento de tarea
│ │ ├── TaskForm.tsx# Formulario de tarea
│ │ └── index.ts
│ │
│ ├── hooks/# Hooks personalizados
│ │ ├── useTasks.ts# Gestión de estado de tareas
│ │ └── index.ts
│ │
│ ├── App.tsx# Aplicación principal
│ ├── App.css# Estilos
│ └── main.tsx# Archivo de entrada
│
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
División de directorios:Dividir los directorios por funcionalidad, separando definiciones de tipos, capa de servicio y componentes.
Definiciones de tipos
Primero se definen los tipos centrales del proyecto.
src/types/task.ts
export type TaskStatus = "pending" | "in-progress" | "completed";
// Enumeración de prioridades de tarea
export type TaskPriority = "low" | "medium" | "high";
// Definición de la interfaz de tarea
export interface Task {
id: string; // ID de la tarea
title: string; // Título de la tarea
description?: string; // Descripción de la tarea (opcional)
status: TaskStatus; // Estado de la tarea
priority: TaskPriority; // Prioridad de la tarea
createdAt: string; // Fecha de creación
updatedAt: string; // Fecha de actualización
dueDate?: string; // Fecha límite (opcional)
tags?: string[]; // Etiquetas (opcional)
}
// Tipo de entrada para crear tarea
export interface CreateTaskInput {
title: string;
description?: string;
priority: TaskPriority;
dueDate?: string;
tags?: string[];
}
// Tipo de entrada para actualizar tarea
export interface UpdateTaskInput {
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
dueDate?: string;
tags?: string[];
}
// Opciones de filtrado de tareas
export interface TaskFilter {
status?: TaskStatus;
priority?: TaskPriority;
search?: string;
}
Tipos por capas:Definir por separado los tipos de entrada, salida y filtrado para facilitar el mantenimiento.
Definiciones de tipos de API
Definir los tipos relacionados con la API.
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// Metadatos de paginación
export interface PaginationMeta {
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Tipo de respuesta de paginación
export interface PaginatedResponse<T> {
items: T[];
meta: PaginationMeta;
}
// Tipo de error de solicitud
export interface ApiError {
code: string;
message: string;
details?: Record<string, string>;
}
// Tipo de método HTTP
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
// Puntos finales de API relacionados con tareas
export interface TaskEndpoints {
getAll: "/api/tasks";
getById: "/api/tasks/:id";
create: "/api/tasks";
update: "/api/tasks/:id";
delete: "/api/tasks/:id";
}
Tipos de API:Formato de respuesta unificado y tipos de manejo de errores para facilitar la integración entre frontend y backend.
Capa de servicio de tareas
Implementar la lógica de negocio de la gestión de tareas.
src/services/taskService.ts
import {
Task,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus,
TaskPriority
} from "../types/task";
// Generar ID único
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Simular base de datos (almacenamiento en memoria)
let tasks: Task[] = [
{
id: "1",
title: "Aprender TypeScript",
description: "Dominar las características básicas y avanzadas de TypeScript",
status: "completed",
priority: "high",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tags: ["Aprender", "TypeScript"]
},
{
id: "2",
title: "Desarrollar un sistema de gestión de tareas",
description: "Desarrollar con React + TypeScript",
status: "in-progress",
priority: "high",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tags: ["Proyecto", "Práctica"]
}
];
// Clase de servicio de tareas
class TaskService {
// Obtener todas las tareas
getAll(filter?: TaskFilter): Task[] {
let result = [...tasks];
if (filter) {
if (filter.status) {
result = result.filter(t => t.status === filter.status);
}
if (filter.priority) {
result = result.filter(t => t.priority === filter.priority);
}
if (filter.search) {
const search = filter.search.toLowerCase();
result = result.filter(t =>
t.title.toLowerCase().includes(search) ||
t.description?.toLowerCase().includes(search)
);
}
}
return result;
}
// Obtener tarea por ID
getById(id: string): Task | undefined {
return tasks.find(t => t.id === id);
}
// Crear tarea
create(input: CreateTaskInput): Task {
const now = new Date().toISOString();
const task: Task = {
id: generateId(),
title: input.title,
description: input.description,
status: "pending",
priority: input.priority,
createdAt: now,
updatedAt: now,
dueDate: input.dueDate,
tags: input.tags
};
tasks.push(task);
return task;
}
// Actualizar tarea
update(id: string, input: UpdateTaskInput): Task | null {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return null;
const task = tasks[index];
const updated: Task = {
...task,
...input,
updatedAt: new Date().toISOString()
};
tasks[index] = updated;
return updated;
}
// Eliminar tarea
delete(id: string): boolean {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return false;
tasks.splice(index, 1);
return true;
}
// Actualizar estado de la tarea
updateStatus(id: string, status: TaskStatus): Task | null {
return this.update(id, { status });
}
}
// Exportar instancia del servicio
export const taskService = new TaskService();
Capa de servicio:La lógica de negocio se concentra en la capa de servicio, lo que facilita las pruebas y el mantenimiento.
Hooks personalizados
Usar Hooks para gestionar el estado de las tareas.
import { useState, useEffect, useCallback } from "react";
import {
Task,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus,
TaskPriority
} from "../types/task";
import { taskService } from "../services/taskService";
// Tipo de estado que devuelve el Hook
interface UseTasksReturn {
tasks: Task[];
loading: boolean;
error: string | null;
filter: TaskFilter;
// Métodos de operación
createTask: (input: CreateTaskInput) => Promise<void>;
updateTask: (id: string, input: UpdateTaskInput) => Promise<void>;
deleteTask: (id: string) => Promise<void>;
updateStatus: (id: string, status: TaskStatus) => Promise<void>;
setFilter: (filter: TaskFilter) => void;
refresh: () => void;
}
// Inicializar filtro predeterminado
const defaultFilter: TaskFilter = {};
export function useTasks(): UseTasksReturn {
// Estado de la lista de tareas
const [tasks, setTasks] = useState<Task[]>([]);
// Estado de carga
const [loading, setLoading] = useState(true);
// Estado de error
const [error, setError] = useState<string | null>(null);
// Condiciones de filtrado
const [filter, setFilter] = useState<TaskFilter>(defaultFilter);
// Cargar lista de tareas
const loadTasks = useCallback(() => {
setLoading(true);
setError(null);
try {
const data = taskService.getAll(filter);
setTasks(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Error al cargar");
} finally {
setLoading(false);
}
}, [filter]);
// Recargar en la carga inicial y cuando cambie el filtro
useEffect(() => {
loadTasks();
}, [loadTasks]);
// Crear tarea
const createTask = useCallback(async (input: CreateTaskInput) => {
try {
taskService.create(input);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Error al crear");
}
}, [loadTasks]);
// Actualizar tarea
const updateTask = useCallback(async (id: string, input: UpdateTaskInput) => {
try {
taskService.update(id, input);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Error al actualizar");
}
}, [loadTasks]);
// Eliminar tarea
const deleteTask = useCallback(async (id: string) => {
try {
taskService.delete(id);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Error al eliminar");
}
}, [loadTasks]);
// Actualizar estado de la tarea
const updateStatus = useCallback(async (id: string, status: TaskStatus) => {
try {
taskService.updateStatus(id, status);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Error al actualizar el estado");
}
}, [loadTasks]);
// Refrescar lista de tareas
const refresh = useCallback(() => {
loadTasks();
}, [loadTasks]);
return {
tasks,
loading,
error,
filter,
createTask,
updateTask,
deleteTask,
updateStatus,
setFilter,
refresh
};
}
Hooks personalizados:Encapsular la gestión de estado y la lógica de negocio en el Hook hace que los componentes sean más limpios.
Componentes React
Usar Hooks para crear un componente de lista de tareas.
src/components/TaskList.tsx
import React from "react";
import { useTasks } from "../hooks/useTasks";
import { Task, TaskStatus, TaskPriority } from "../types/task";
// Props del componente de elemento de tarea
interface TaskItemProps {
task: Task;
onStatusChange: (id: string, status: TaskStatus) => void;
onDelete: (id: string) => void;
}
// Componente de elemento de tarea
const TaskItem: React.FC<TaskItemProps> = ({
task,
onStatusChange,
onDelete
}) => {
// Mapeo de estilos de estado
const statusStyles: Record<TaskStatus, string> = {
"pending": "status-pending",
"in-progress": "status-progress",
"completed": "status-completed"
};
// Mapeo de estilos de prioridad
const priorityLabels: Record<TaskPriority, string> = {
"low": "Baja",
"medium": "Media",
"high": "Alta"
};
return (
<div className={`task-item ${statusStyles[task.status]}`}>
<div className="task-content">
<h3 className="task-title">{task.title}</h3>
{task.description && (
<p className="task-description">{task.description}</p>
)}
<div className="task-meta">
<span className={`priority priority-${task.priority}`}>
{priorityLabels[task.priority]}
</span>
<span className="task-date">
{new Date(task.createdAt).toLocaleDateString()}
</span>
</div>
</div>
<div className="task-actions">
<select
value={task.status}
onChange={(e) => onStatusChange(task.id, e.target.value as TaskStatus)}
className="status-select"
>
<option value="pending">Pendiente</option>
<option value="in-progress">En progreso</option>
<option value="completed">Completada</option>
</select>
<button
onClick={() => onDelete(task.id)}
className="delete-btn"
>
Eliminar
</button>
</div>
</div>
);
};
// Componente de lista de tareas
export const TaskList: React.FC = () => {
// Usar el Hook personalizado para obtener el estado de las tareas
const {
tasks,
loading,
error,
updateStatus,
deleteTask
} = useTasks();
// Renderizar estado de carga
if (loading) {
return <div className="loading">Cargando...</div>;
}
// Renderizar estado de error
if (error) {
return <div className="error">Error: {error}</div>;
}
// Renderizar estado vacío
if (tasks.length === 0) {
return (
<div className="empty">
<p>No hay tareas</p>
<p>Haz clic en el botón superior para crear una nueva tarea</p>
</div>
);
}
// Renderizar lista de tareas
return (
<div className="task-list">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onStatusChange={updateStatus}
onDelete={deleteTask}
/>
))}
</div>
);
};
export default TaskList;
División de componentes:Dividir TaskItem en un componente independiente hace que el código sea más claro.
Componente principal de la aplicación
Integrar todos los componentes para formar una aplicación completa.
src/App.tsx
import React, { useState } from "react";
import { TaskList } from "./components/TaskList";
import { useTasks } from "./hooks/useTasks";
import { CreateTaskInput, TaskPriority } from "./types/task";
import "./App.css";
// Props del componente de formulario de tarea
interface TaskFormProps {
onSubmit: (input: CreateTaskInput) => void;
}
// Componente de formulario de tarea
const TaskForm: React.FC<TaskFormProps> = ({ onSubmit }) => {
// Estado del formulario
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [priority, setPriority] = useState<TaskPriority>("medium");
// Manejo del envío
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
alert("Por favor, ingresa el título de la tarea");
return;
}
onSubmit({
title: title.trim(),
description: description.trim() || undefined,
priority
});
// Restablecer formulario
setTitle("");
setDescription("");
setPriority("medium");
};
return (
<form onSubmit={handleSubmit} className="task-form">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ingresa el título de la tarea"
className="form-input"
/>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Ingresa la descripción de la tarea (opcional)"
className="form-input"
/>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as TaskPriority)}
className="form-select"
>
<option value="low">Prioridad baja</option>
<option value="medium">Prioridad media</option>
<option value="high">Prioridad alta</option>
</select>
<button type="submit" className="submit-btn">
Agregar tarea
</button>
</form>
);
};
// Componente principal de la aplicación
const App: React.FC = () => {
// Usar el Hook personalizado
const { createTask, error } = useTasks();
// Renderizar mensaje de error
const renderError = () => {
if (!error) return null;
return <div className="app-error">{error}</div>;
};
return (
<div className="app">
<header className="app-header">
<h1>Sistema de gestión de tareas en TypeScript</h1>
<p>Proyecto práctico integral</p>
</header>
{renderError()}
<main className="app-main">
<TaskForm onSubmit={createTask} />
<TaskList />
</main>
<footer className="app-footer">
<p>Powered by TypeScript + React</p>
</footer>
</div>
);
};
export default App;
Composición de componentes:Construir interfaces de usuario complejas mediante la composición de componentes simples.
Configuración del proyecto
La configuración de TypeScript y de compilación del proyecto.
tsconfig.json
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"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
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Modo estricto:Activa `strict` para obtener la verificación de tipos más completa.
Notas importantes
- Priorizar los tipos:Define primero los tipos y luego escribe el código de implementación.
- Interfaces vs. tipos:Usa interfaces para tipos de objeto y alias de tipo para tipos unión.
- Arquitectura en capas:Organiza por capas: tipos, servicios y componentes.
- Encapsulación de Hooks:Encapsula la lógica de negocio en Hooks.
Mejores prácticas:La definición de tipos es la base de un proyecto TypeScript; debe diseñarse cuidadosamente.
Resumen
Con este proyecto integral, pusimos en práctica los conceptos centrales de TypeScript.
- Definición de tipos:Interfaces como `Task`, `CreateTaskInput`, `TaskFilter`, etc.
- Capa de servicios:`TaskService` encapsula la lógica de negocio.
- Hooks personalizados:`useTasks` administra el estado.
- Componentes de React:Desarrollo de componentes con seguridad de tipos.
- Configuración del proyecto:Configuración estricta de TypeScript.
Otras extensionesSugerencias:Participa en más proyectos reales y profundiza tu comprensión de TypeScript en la práctica.