Umfassende Projektpraxis mit TypeScript
Dieses Tutorial verwendet ein vollständiges Projektbeispiel, um verschiedene Funktionen von TypeScript umfassend anzuwenden.
Von der Projekteinrichtung bis zur tatsächlichen Entwicklung zeigt es vollständig die Anwendung von TypeScript in realen Projekten.
SVG-Diagramm: Projektarchitektur
Warum brauchen wir umfassende Projektpraxis?
Nachdem Sie die TypeScript-Syntax gelernt haben, müssen Sie Ihr Wissen durch reale Projekte festigen.
Dieses Tutorial demonstriert ein vollständiges Aufgabenverwaltungssystem, das Frontend, Backend und Typdefinitionen abdeckt.
Durch dieses Projekt können Sie die Best Practices von TypeScript in der realen Entwicklung beherrschen.
Projektziele:Erstellen Sie ein Aufgabenverwaltungssystem mit Funktionen zum Erstellen, Abfragen, Aktualisieren und Löschen von Aufgaben.
Projektstruktur
Organisieren Sie die Projektstruktur im Monorepo-Stil.
Verzeichnisstruktur
├── src/
│ ├── types/# Typdefinitionen
│ │ ├── task.ts# Task-Typen
│ │ ├── api.ts# API-Typen
│ │ └── index.ts# Typ-Exporte
│ │
│ ├── services/# Service-Schicht
│ │ ├── taskService.ts# Task-Service
│ │ └── index.ts
│ │
│ ├── components/# React-Komponenten
│ │ ├── TaskList.tsx# Task-Liste
│ │ ├── TaskItem.tsx# Task-Element
│ │ ├── TaskForm.tsx# Task-Formular
│ │ └── index.ts
│ │
│ ├── hooks/# Benutzerdefinierte Hooks
│ │ ├── useTasks.ts# Task-Zustandsverwaltung
│ │ └── index.ts
│ │
│ ├── App.tsx# Haupt-App
│ ├── App.css# Stile
│ └── main.tsx# Einstiegsdatei
│
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts
Verzeichnisaufteilung:Verzeichnisse werden nach Funktion aufgeteilt, wobei Typdefinitionen, Service-Schicht und Komponenten getrennt werden.
Typdefinitionen
Definiere zuerst die Kerntypen des Projekts.
src/types/task.ts
export type TaskStatus = "pending" | "in-progress" | "completed";
// Task-Prioritäts-Enum
export type TaskPriority = "low" | "medium" | "high";
// Task-Interface-Definition
export interface Task {
id: string; // Task-ID
title: string; // Task-Titel
description?: string; // Task-Beschreibung (optional)
status: TaskStatus; // Task-Status
priority: TaskPriority; // Task-Priorität
createdAt: string; // Erstellungszeit
updatedAt: string; // Aktualisierungszeit
dueDate?: string; // Fälligkeitsdatum (optional)
tags?: string[]; // Schlagwörter (optional)
}
// Eingabetyp zum Erstellen einer Aufgabe
export interface CreateTaskInput {
title: string;
description?: string;
priority: TaskPriority;
dueDate?: string;
tags?: string[];
}
// Eingabetyp zum Aktualisieren einer Aufgabe
export interface UpdateTaskInput {
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
dueDate?: string;
tags?: string[];
}
// Task-Filteroptionen
export interface TaskFilter {
status?: TaskStatus;
priority?: TaskPriority;
search?: string;
}
Typenschichtung:Definieren Sie Eingabetypen, Ausgabetypen und Filtertypen getrennt, um die Wartung zu erleichtern.
API-Typdefinitionen
Definieren Sie API-bezogene Typen.
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// Paginierungs-Metadaten
export interface PaginationMeta {
total: number;
page: number;
pageSize: number;
totalPages: number;
}
// Paginierungs-Antworttyp
export interface PaginatedResponse<T> {
items: T[];
meta: PaginationMeta;
}
// Anfragefehlertyp
export interface ApiError {
Code: string;
message: string;
details?: Record<string, string>;
}
// HTTP-Methodentyp
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
// Task-bezogene API-Endpunkte
export interface TaskEndpoints {
getAll: "/api/tasks";
getById: "/api/tasks/:id";
create: "/api/tasks";
update: "/api/tasks/:id";
delete: "/api/tasks/:id";
}
API-Typen:Einheitliches Antwortformat und Fehlerbehandlungstypen erleichtern die Frontend-Backend-Integration.
Task-Service-Schicht
Implementiere die Geschäftslogik für die Aufgabenverwaltung.
src/services/taskService.ts
import {
Aufgabe,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus,
TaskPriority
} from "../types/task";
// Eindeutige ID generieren
function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
// Mock-Datenbank (In-Memory-Speicher)
let tasks: Task[] = [
{
id: "1",
title: "TypeScript lernen",
description: "TypeScript-Grundlagen und fortgeschrittene Features meistern",
status: "abgeschlossen",
priority: "hoch",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tags: ["Lernen", "TypeScript"]
},
{
id: "2",
title: "Ein Aufgabenverwaltungssystem entwickeln",
description: "Mit React + TypeScript entwickeln",
status: "in Bearbeitung",
priority: "hoch",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
tags: ["Project", "Practice"]
}
];
// Task-Service-Klasse
class TaskService {
// Alle Aufgaben abrufen
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;
}
// Aufgabe nach ID abrufen
getById(id: string): Task | undefined {
return tasks.find(t => t.id === id);
}
// Aufgabe erstellen
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;
}
// Aufgabe aktualisieren
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;
}
// Aufgabe löschen
delete(id: string): boolean {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return false;
tasks.splice(index, 1);
return true;
}
// Aufgabenstatus aktualisieren
updateStatus(id: string, status: TaskStatus): Task | null {
return this.update(id, { status });
}
}
// Serviceinstanz exportieren
export const taskService = new TaskService();
Service-Schicht:Die Geschäftslogik ist in der Service-Schicht zentralisiert, um einfaches Testen und Warten zu ermöglichen.
Benutzerdefinierter Hook
Hook zum Verwalten des Aufgabenstatus verwenden.
import { useState, useEffect, useCallback } from "react";
import {
Aufgabe,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus,
TaskPriority
} from "../types/task";
import { taskService } from "../services/taskService";
// Vom Hook zurückgegebener Zustandstyp
interface UseTasksReturn {
tasks: Task[];
loading: boolean;
error: string | null;
filter: TaskFilter;
// Operationsmethoden
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;
}
// Standardfilter initialisieren
const defaultFilter: TaskFilter = {};
export function useTasks(): UseTasksReturn {
// Status der Aufgabenliste
const [tasks, setTasks] = useState<Task[]>([]);
// Ladezustand
const [loading, setLoading] = useState(true);
// Fehlerzustand
const [error, setError] = useState<string | null>(null);
// Filterbedingungen
const [filter, setFilter] = useState<TaskFilter>(defaultFilter);
// Aufgabenliste laden
const loadTasks = useCallback(() => {
setLoading(true);
setError(null);
try {
const data = taskService.getAll(filter);
setTasks(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Laden fehlgeschlagen");
} finally {
setLoading(false);
}
}, [filter]);
// Beim initialen Laden und bei Filteränderungen neu laden
useEffect(() => {
loadTasks();
}, [loadTasks]);
// Aufgabe erstellen
const createTask = useCallback(async (input: CreateTaskInput) => {
try {
taskService.create(input);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Erstellen fehlgeschlagen");
}
}, [loadTasks]);
// Aufgabe aktualisieren
const updateTask = useCallback(async (id: string, input: UpdateTaskInput) => {
try {
taskService.update(id, input);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Aktualisieren fehlgeschlagen");
}
}, [loadTasks]);
// Aufgabe löschen
const deleteTask = useCallback(async (id: string) => {
try {
taskService.delete(id);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Löschen fehlgeschlagen");
}
}, [loadTasks]);
// Aufgabenstatus aktualisieren
const updateStatus = useCallback(async (id: string, status: TaskStatus) => {
try {
taskService.updateStatus(id, status);
loadTasks();
} catch (err) {
setError(err instanceof Error ? err.message : "Statusaktualisierung fehlgeschlagen");
}
}, [loadTasks]);
// Aufgabenliste aktualisieren
const refresh = useCallback(() => {
loadTasks();
}, [loadTasks]);
return {
tasks,
loading,
error,
filter,
createTask,
updateTask,
deleteTask,
updateStatus,
setFilter,
refresh
};
}
Benutzerdefinierter Hook:Kapseln Sie die Zustandsverwaltung und die Geschäftslogik in einem Hook, wodurch Komponenten prägnanter werden.
React-Komponenten
Verwende den Hook, um eine Aufgabenlisten-Komponente zu erstellen.
src/components/TaskList.tsx
import React from "react";
import { useTasks } from "../hooks/useTasks";
import { Task, TaskStatus, TaskPriority } from "../types/task";
// Props der Aufgaben-Element-Komponente
interface TaskItemProps {
task: Task;
onStatusChange: (id: string, status: TaskStatus) => void;
onDelete: (id: string) => void;
}
// Aufgaben-Element-Komponente
const TaskItem: React.FC<TaskItemProps> = ({
task,
onStatusChange,
onDelete
}) => {
// Status-Stil-Zuordnung
const statusStyles: Record<TaskStatus, string> = {
"pending": "status-pending",
"in-progress": "status-progress",
"completed": "status-completed"
};
// Prioritätsstil-Zuordnung
const priorityLabels: Record<TaskPriority, string> = {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
};
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">Ausstehend</option>
<option value="in-progress">In Bearbeitung</option>
<option value="completed">Abgeschlossen</option>
</select>
<button
onClick={() => onDelete(task.id)}
className="delete-btn"
>
Delete
</button>
</div>
</div>
);
};
// Aufgabenlisten-Komponente
export const TaskList: React.FC = () => {
// Benutzerdefinierten Hook verwenden, um Aufgabenstatus abzurufen
const {
tasks,
loading,
error,
updateStatus,
deleteTask
} = useTasks();
// Ladezustand rendern
if (loading) {
return <div className="loading">Wird geladen...</div>;
}
// Fehlerzustand rendern
if (error) {
return <div className="error">Fehler: {error}</div>;
}
// Leerzustand rendern
if (tasks.length === 0) {
return (
<div className="empty">
<p>Noch keine Aufgaben</p>
<p>Klicke auf den Button oben, um eine neue Aufgabe zu erstellen</p>
</div>
);
}
// Aufgabenliste rendern
return (
<div className="task-list">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onStatusChange={updateStatus}
onDelete={deleteTask}
/>
))}
</div>
);
};
export default TaskList;
Komponentenaufteilung:TaskItem in eine unabhängige Komponente aufteilen, wodurch der Code klarer wird.
Haupt-App-Komponente
Alle Komponenten integrieren, um eine vollständige Anwendung zu bilden.
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 der Task-Formular-Komponente
interface TaskFormProps {
onSubmit: (input: CreateTaskInput) => void;
}
// Task-Formular-Komponente
const TaskForm: React.FC<TaskFormProps> = ({ onSubmit }) => {
// Formularzustand
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [priority, setPriority] = useState<TaskPriority>("medium");
// Absenden behandeln
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) {
alert("Bitte geben Sie einen Aufgabentitel ein");
return;
}
onSubmit({
title: title.trim(),
description: description.trim() || undefined,
priority
});
// Formular zurücksetzen
setTitle("");
setDescription("");
setPriority("medium");
};
return (
<form onSubmit={handleSubmit} className="task-form">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Aufgabentitel eingeben"
className="form-input"
/>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Aufgabenbeschreibung eingeben (optional)"
className="form-input"
/>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as TaskPriority)}
className="form-select"
>
<option value="low">Niedrige Priorität</option>
<option value="medium">Mittlere Priorität</option>
<option value="high">Hohe Priorität</option>
</select>
<button type="submit" className="submit-btn">
Aufgabe hinzufügen
</button>
</Form>
);
};
// Haupt-App-Komponente
const App: React.FC = () => {
// Benutzerdefinierten Hook verwenden
const { createTask, error } = useTasks();
// Fehlermeldung rendern
const renderError = () => {
if (!error) return null;
return <div className="app-error">{error}</div>;
};
return (
<div className="app">
<header className="app-header">
<h1>TypeScript-Aufgabenverwaltungssystem</h1>
<p>Umfassendes Übungsprojekt</p>
</header>
{renderError()}
<main className="app-main">
<TaskForm onSubmit={createTask} />
<TaskList />
</main>
<footer className="app-footer">
<p>Bereitgestellt von TypeScript + React</p>
</footer>
</div>
);
};
export default App;
Komponentenkomposition:Komplexe UI durch Zusammensetzen einfacher Komponenten erstellen.
Projektkonfiguration
Die TypeScript- und Build-Konfiguration des Projekts.
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" }]
}
Strict-Modus:Aktiviere strict für die umfassendste Typprüfung.
Hinweise
- Typen zuerst:Definiere zuerst Typen, dann schreibe Implementierungscode.
- Interface vs. Typ:Verwende Interfaces für Objekttypen, Typaliase für Union-Typen.
- Schichtenarchitektur:Organisiere Typen, Services und Komponenten in Schichten.
- Hook-Kapselung:Geschäftslogik in Hooks kapseln.
Best Practices:Typdefinitionen sind die Grundlage eines TypeScript-Projekts; entwerfen Sie sie sorgfältig.
Zusammenfassung
In diesem umfassenden Projekt haben wir die Kernkonzepte von TypeScript geübt.
- Typdefinitionen:Interfaces wie Task, CreateTaskInput, TaskFilter.
- Service-Schicht:TaskService kapselt Geschäftslogik.
- Benutzerdefinierte Hooks:useTasks verwaltet den Status.
- React-Komponenten:Typsichere Komponentenentwicklung.
- Projektkonfiguration:Strikte TypeScript-Konfiguration.
Weitere ErweiterungenEmpfehlungen:Nehmen Sie an mehr echten Projekten teil, um Ihr Verständnis von TypeScript durch Praxis zu vertiefen.