TypeScript Final Project - Todo App mit Type Safety
Zeit für ein echtes Projekt! Wir bauen eine vollständige Todo-App mit allem was du gelernt hast.
Projekt-Übersicht
Was wir bauen:
- ✅ Todo hinzufügen, bearbeiten, löschen
- ✅ Kategorien und Prioritäten
- ✅ Filter und Suche
- ✅ LocalStorage Persistence
- ✅ 100% Type-Safe
Tech Stack:
- TypeScript (strict mode)
- Vanilla JavaScript (DOM)
- LocalStorage API
- CSS für Styling
Projekt-Setup
mkdir typescript-todo-app
cd typescript-todo-app
npm init -y
npm install --save-dev typescript
npx tsc --init
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Types definieren
// src/types.ts
export enum Priority {
Low = "low",
Medium = "medium",
High = "high"
}
export enum Category {
Work = "work",
Personal = "personal",
Shopping = "shopping",
Health = "health"
}
export interface Todo {
id: string
title: string
description?: string
completed: boolean
priority: Priority
category: Category
createdAt: Date
updatedAt: Date
dueDate?: Date
}
export interface TodoFilter {
category?: Category
priority?: Priority
completed?: boolean
searchTerm?: string
}
export type TodoUpdate = Partial<Omit<Todo, "id" | "createdAt">>
Storage Service
// src/storage.ts
import { Todo } from "./types"
const STORAGE_KEY = "typescript-todos"
export class StorageService {
static saveTodos(todos: Todo[]): void {
try {
const serialized = JSON.stringify(todos)
localStorage.setItem(STORAGE_KEY, serialized)
} catch (error) {
console.error("Failed to save todos:", error)
}
}
static loadTodos(): Todo[] {
try {
const serialized = localStorage.getItem(STORAGE_KEY)
if (!serialized) return []
const parsed = JSON.parse(serialized)
// Convert date strings back to Date objects
return parsed.map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
updatedAt: new Date(todo.updatedAt),
dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined
}))
} catch (error) {
console.error("Failed to load todos:", error)
return []
}
}
static clearTodos(): void {
localStorage.removeItem(STORAGE_KEY)
}
}
Todo Manager
// src/todoManager.ts
import { Todo, TodoFilter, TodoUpdate, Priority, Category } from "./types"
import { StorageService } from "./storage"
export class TodoManager {
private todos: Todo[] = []
constructor() {
this.todos = StorageService.loadTodos()
}
addTodo(
title: string,
priority: Priority = Priority.Medium,
category: Category = Category.Personal
): Todo {
const todo: Todo = {
id: this.generateId(),
title,
description: undefined,
completed: false,
priority,
category,
createdAt: new Date(),
updatedAt: new Date()
}
this.todos.push(todo)
this.save()
return todo
}
updateTodo(id: string, updates: TodoUpdate): Todo | null {
const todo = this.findById(id)
if (!todo) return null
Object.assign(todo, {
...updates,
updatedAt: new Date()
})
this.save()
return todo
}
deleteTodo(id: string): boolean {
const index = this.todos.findIndex(t => t.id === id)
if (index === -1) return false
this.todos.splice(index, 1)
this.save()
return true
}
toggleComplete(id: string): Todo | null {
const todo = this.findById(id)
if (!todo) return null
todo.completed = !todo.completed
todo.updatedAt = new Date()
this.save()
return todo
}
getTodos(filter?: TodoFilter): Todo[] {
let filtered = [...this.todos]
if (filter) {
if (filter.category) {
filtered = filtered.filter(t => t.category === filter.category)
}
if (filter.priority) {
filtered = filtered.filter(t => t.priority === filter.priority)
}
if (filter.completed !== undefined) {
filtered = filtered.filter(t => t.completed === filter.completed)
}
if (filter.searchTerm) {
const term = filter.searchTerm.toLowerCase()
filtered = filtered.filter(t =>
t.title.toLowerCase().includes(term) ||
t.description?.toLowerCase().includes(term)
)
}
}
return filtered.sort((a, b) =>
b.createdAt.getTime() - a.createdAt.getTime()
)
}
getStats() {
return {
total: this.todos.length,
completed: this.todos.filter(t => t.completed).length,
pending: this.todos.filter(t => !t.completed).length,
byCategory: {
work: this.todos.filter(t => t.category === Category.Work).length,
personal: this.todos.filter(t => t.category === Category.Personal).length,
shopping: this.todos.filter(t => t.category === Category.Shopping).length,
health: this.todos.filter(t => t.category === Category.Health).length
}
}
}
private findById(id: string): Todo | undefined {
return this.todos.find(t => t.id === id)
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
private save(): void {
StorageService.saveTodos(this.todos)
}
}
UI Renderer
// src/ui.ts
import { TodoManager } from "./todoManager"
import { Todo, Priority, Category } from "./types"
export class UIRenderer {
private todoManager: TodoManager
private filterCategory?: Category
private filterCompleted?: boolean
constructor(todoManager: TodoManager) {
this.todoManager = todoManager
this.setupEventListeners()
this.render()
}
private setupEventListeners(): void {
// Add Todo Form
const form = document.getElementById("add-todo-form") as HTMLFormElement
form?.addEventListener("submit", (e) => {
e.preventDefault()
this.handleAddTodo()
})
// Filter Buttons
document.querySelectorAll("[data-filter]").forEach(btn => {
btn.addEventListener("click", () => {
const filter = btn.getAttribute("data-filter")
this.handleFilter(filter)
})
})
}
private handleAddTodo(): void {
const titleInput = document.getElementById("todo-title") as HTMLInputElement
const prioritySelect = document.getElementById("todo-priority") as HTMLSelectElement
const categorySelect = document.getElementById("todo-category") as HTMLSelectElement
const title = titleInput.value.trim()
if (!title) return
this.todoManager.addTodo(
title,
prioritySelect.value as Priority,
categorySelect.value as Category
)
titleInput.value = ""
this.render()
}
private handleFilter(filter: string | null): void {
switch (filter) {
case "all":
this.filterCompleted = undefined
break
case "active":
this.filterCompleted = false
break
case "completed":
this.filterCompleted = true
break
}
this.render()
}
private render(): void {
this.renderTodos()
this.renderStats()
}
private renderTodos(): void {
const container = document.getElementById("todo-list")
if (!container) return
const todos = this.todoManager.getTodos({
category: this.filterCategory,
completed: this.filterCompleted
})
if (todos.length === 0) {
container.innerHTML = `
<div class="empty-state">
<p>Keine Todos gefunden</p>
</div>
`
return
}
container.innerHTML = todos.map(todo => this.todoTemplate(todo)).join("")
// Event Listeners für Todo-Items
container.querySelectorAll("[data-todo-id]").forEach(el => {
const id = el.getAttribute("data-todo-id")!
el.querySelector("[data-action='toggle']")?.addEventListener("click", () => {
this.todoManager.toggleComplete(id)
this.render()
})
el.querySelector("[data-action='delete']")?.addEventListener("click", () => {
if (confirm("Todo löschen?")) {
this.todoManager.deleteTodo(id)
this.render()
}
})
})
}
private todoTemplate(todo: Todo): string {
const priorityClass = `priority-${todo.priority}`
const completedClass = todo.completed ? "completed" : ""
return `
<div class="todo-item ${priorityClass} ${completedClass}" data-todo-id="${todo.id}">
<div class="todo-checkbox">
<input type="checkbox" ${todo.completed ? "checked" : ""} data-action="toggle">
</div>
<div class="todo-content">
<h3>${this.escapeHtml(todo.title)}</h3>
<div class="todo-meta">
<span class="category">${todo.category}</span>
<span class="priority">${todo.priority}</span>
<span class="date">${this.formatDate(todo.createdAt)}</span>
</div>
</div>
<button class="btn-delete" data-action="delete">×</button>
</div>
`
}
private renderStats(): void {
const stats = this.todoManager.getStats()
const container = document.getElementById("stats")
if (!container) return
container.innerHTML = `
<div class="stat">
<span class="stat-value">${stats.total}</span>
<span class="stat-label">Gesamt</span>
</div>
<div class="stat">
<span class="stat-value">${stats.pending}</span>
<span class="stat-label">Offen</span>
</div>
<div class="stat">
<span class="stat-value">${stats.completed}</span>
<span class="stat-label">Erledigt</span>
</div>
`
}
private escapeHtml(text: string): string {
const div = document.createElement("div")
div.textContent = text
return div.innerHTML
}
private formatDate(date: Date): string {
return date.toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric"
})
}
}
Main Entry Point
// src/main.ts
import { TodoManager } from "./todoManager"
import { UIRenderer } from "./ui"
// Initialize App
const todoManager = new TodoManager()
const ui = new UIRenderer(todoManager)
console.log("Todo App started!")
HTML Structure
<!-- index.html -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TypeScript Todo App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<h1>📝 Todo App</h1>
<div id="stats"></div>
</header>
<main>
<form id="add-todo-form">
<input
type="text"
id="todo-title"
placeholder="Neues Todo..."
required
>
<select id="todo-priority">
<option value="low">Niedrig</option>
<option value="medium" selected>Mittel</option>
<option value="high">Hoch</option>
</select>
<select id="todo-category">
<option value="personal">Privat</option>
<option value="work">Arbeit</option>
<option value="shopping">Einkauf</option>
<option value="health">Gesundheit</option>
</select>
<button type="submit">Hinzufügen</button>
</form>
<div class="filters">
<button data-filter="all">Alle</button>
<button data-filter="active">Offen</button>
<button data-filter="completed">Erledigt</button>
</div>
<div id="todo-list"></div>
</main>
</div>
<script type="module" src="dist/main.js"></script>
</body>
</html>
Build & Run
package.json Scripts:
{
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"dev": "tsc --watch"
}
}
Build:
npm run build
Run: Öffne index.html im Browser
Zusammenfassung
Du hast gebaut:
- ✅ Type-Safe Todo App
- ✅ CRUD Operations
- ✅ LocalStorage Persistence
- ✅ Filter & Search
- ✅ Stats Dashboard
- ✅ Clean Architecture
Verwendete TypeScript Features:
- Enums für Konstanten
- Interfaces für Daten-Strukturen
- Utility Types (Partial, Omit)
- Type Guards
- Generics
- Classes mit private/public
Nächste Schritte:
- ✨ Drag & Drop für Sortierung
- ✨ Due Dates mit Notifications
- ✨ Tags System
- ✨ Export/Import JSON
- ✨ Dark Mode
- ✨ React/Vue Version
Herzlichen Glückwunsch! Du hast eine vollständige TypeScript-App gebaut! 🎉
Gut gemacht! 🎉
Du hast "TypeScript Final Project - Todo App mit Type Safety" abgeschlossen
Artikel bewerten
Bitte einloggen um zu bewerten
Das könnte dich auch interessieren
TypeScript Basic Types - Primitive Datentypen
Lerne die grundlegenden Datentypen in TypeScript kennen: string, number, boolean, arrays, tuples und mehr.
TypeScript Best Practices - Professioneller Code
Lerne die wichtigsten Best Practices für sauberen und wartbaren TypeScript Code. Naming, Patterns, Do's and Don'ts.
TypeScript Classes - Objektorientierte Programmierung
Lerne Classes in TypeScript mit Access Modifiers, Constructors, Inheritance, Abstract Classes und mehr.