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 Fondo Título Proyecto práctico: Sistema de gestión de tareas Frontend Frontend (React + TS) Vite + React 18 Componentes + Hooks UI con seguridad de tipos Flecha Backend Backend (Node.js) Express + TS RESTful API Definiciones de tipos Flecha Datos Almacenamiento de datos In-Memory (Demostración) Parte inferior: Funcionalidades Características funcionales del proyecto Funcionalidad 1 Operaciones CRUD de tareas Funcionalidad 2 API con seguridad de tipos Funcionalidad 3 Gestión de estado Marcador de flecha

¿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

task-manager/
├── 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

// Enumeración de estados de tarea
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.

< h2 class="example">src/types/api.ts
// Tipo de respuesta genérica de 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

// Importar definiciones de tipos
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.

< h2 class="example">src/hooks/useTasks.ts
// Importar React Hooks y tipos
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

// Importar React y el Hook personalizado
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

// Importar React y tipos
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.

Sugerencias:Participa en más proyectos reales y profundiza tu comprensión de TypeScript en la práctica.

Otras extensiones