TypeScript: Projeto Abrangente em TypeScript
Última atualização: 2026-08-26
Agora que você concluiu as primeiras 29 lições, é hora de reunir todo esse conhecimento — nesta lição, vamos criar um aplicativo Todo de linha de comando (CLI) do zero usando TypeScript, abordando conceitos fundamentais como design de tipos, genéricos, modularidade e tratamento de erros.
1. Visão geral do projeto
(1) Requisitos funcionais
Todo CLI Tools
├── Add a Task(add)
├── List Tasks(list)
├── Complete the task(done)
├── Delete Task(delete)
├── Filter by Status(list --done / list --pending)
└── Data Persistence(JSON File Storage)
(2) Requisitos técnicos
- TypeScript puro, sem dependências de terceiros
- Design modular — Estratificação de tipos, armazenamento, lógica de negócios e CLI
- Segurança de tipos total — sem
any - Tratamento de erros no modo de resultados
- Camada de armazenamento genérica reutilizável
2. Design tipográfico
(1) Definições de tipos de núcleo
// types.ts
type Priority = "low" | "medium" | "high";
type TodoStatus = "pending" | "done";
interface Todo {
id: string;
title: string;
description?: string;
priority: Priority;
status: TodoStatus;
createdAt: string;
updatedAt: string;
}
// Derivation Using Tool Types——DRY Principles
type CreateTodo = Omit<Todo, "id" | "createdAt" | "updatedAt">;
type UpdateTodo = Partial<Omit<Todo, "id" | "createdAt">>;
interface TodoFilter {
status?: TodoStatus;
priority?: Priority;
}
type SortBy = "createdAt" | "priority" | "title";
type SortOrder = "asc" | "desc";
interface TodoSort {
by: SortBy;
order: SortOrder;
}
(2) Padrão de resultados
// result.ts
type Success<T> = { ok: true; value: T };
type Failure<E> = { ok: false; error: E };
type Result<T, E = string> = Success<T> | Failure<E>;
function ok<T>(value: T): Success<T> {
return { ok: true, value };
}
function err<E>(error: E): Failure<E> {
return { ok: false, error };
}
3. Camada de armazenamento genérica
(1) Interface de armazenamento
// storage.ts
interface Storage<T extends { id: string }> {
getAll(): Result<T[]>;
getById(id: string): Result<T | null>;
save(item: T): Result<T>;
delete(id: string): Result<boolean>;
find(predicate: (item: T) => boolean): Result<T[]>;
}
(2) Implementação do armazenamento de arquivos JSON
class JsonFileStorage<T extends { id: string }> implements Storage<T> {
private data: Map<string, T> = new Map();
private loaded: boolean = false;
constructor(private filePath: string) {}
private load(): Result<void> {
if (this.loaded) return ok(undefined);
try {
let fs = require("fs");
if (fs.existsSync(this.filePath)) {
let raw = fs.readFileSync(this.filePath, "utf-8");
let items: T[] = JSON.parse(raw);
items.forEach(item => this.data.set(item.id, item));
}
this.loaded = true;
return ok(undefined);
} catch (error) {
return err(`Failed to load:${error instanceof Error ? error.message : String(error)}`);
}
}
private persist(): Result<void> {
try {
let fs = require("fs");
let items = Array.from(this.data.values());
fs.writeFileSync(this.filePath, JSON.stringify(items, null, 2), "utf-8");
return ok(undefined);
} catch (error) {
return err(`Save Failed:${error instanceof Error ? error.message : String(error)}`);
}
}
getAll(): Result<T[]> {
let r = this.load();
if (!r.ok) return err(r.error);
return ok(Array.from(this.data.values()));
}
getById(id: string): Result<T | null> {
let r = this.load();
if (!r.ok) return err(r.error);
return ok(this.data.get(id) ?? null);
}
save(item: T): Result<T> {
let r = this.load();
if (!r.ok) return err(r.error);
this.data.set(item.id, { ...item, updatedAt: new Date().toISOString() } as T);
let p = this.persist();
if (!p.ok) return err(p.error);
return ok({ ...item, updatedAt: new Date().toISOString() } as T);
}
delete(id: string): Result<boolean> {
let r = this.load();
if (!r.ok) return err(r.error);
let existed = this.data.delete(id);
let p = this.persist();
if (!p.ok) return err(p.error);
return ok(existed);
}
find(predicate: (item: T) => boolean): Result<T[]> {
let r = this.getAll();
if (!r.ok) return err(r.error);
return ok(r.value.filter(predicate));
}
}
4. Camada de lógica de negócios
// service.ts
class TodoService {
private idCounter: number = 0;
constructor(private storage: Storage<Todo>) {}
private generateId(): string {
return `todo_${Date.now()}_${++this.idCounter}`;
}
add(input: CreateTodo): Result<Todo> {
let now = new Date().toISOString();
let todo: Todo = {
id: this.generateId(),
title: input.title,
description: input.description,
priority: input.priority,
status: "pending",
createdAt: now,
updatedAt: now
};
return this.storage.save(todo);
}
list(filter?: TodoFilter, sort?: TodoSort): Result<Todo[]> {
let r = this.storage.getAll();
if (!r.ok) return err(r.error);
let todos = r.value;
// Filter
if (filter?.status) todos = todos.filter(t => t.status === filter.status);
if (filter?.priority) todos = todos.filter(t => t.priority === filter.priority);
// Sort
let sortBy: SortBy = sort?.by ?? "createdAt";
let order: SortOrder = sort?.order ?? "desc";
let priorityOrder: Record<Priority, number> = { high: 3, medium: 2, low: 1 };
todos.sort((a, b) => {
let cmp = 0;
if (sortBy === "createdAt") cmp = a.createdAt.localeCompare(b.createdAt);
else if (sortBy === "priority") cmp = priorityOrder[a.priority] - priorityOrder[b.priority];
else if (sortBy === "title") cmp = a.title.localeCompare(b.title);
return order === "asc" ? cmp : -cmp;
});
return ok(todos);
}
done(id: string): Result<Todo> {
let r = this.storage.getById(id);
if (!r.ok) return err(r.error);
if (!r.value) return err(`Task ${id} Does not exist`);
if (r.value.status === "done") return err(`Task ${id} Completed`);
return this.storage.save({ ...r.value, status: "done" });
}
delete(id: string): Result<boolean> {
return this.storage.delete(id);
}
update(id: string, updates: UpdateTodo): Result<Todo> {
let r = this.storage.getById(id);
if (!r.ok) return err(r.error);
if (!r.value) return err(`Task ${id} Does not exist`);
return this.storage.save({ ...r.value, ...updates });
}
}
▶ Exemplo: Testes na camada de serviço
// Usage——Create a service and perform an operation
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);
// Add a Task
let addResult = service.add({ title: "StudyTypeScript", priority: "high" });
if (addResult.ok) {
console.log(`Added:${addResult.value.title}`);
}
// List Tasks
let listResult = service.list({ status: "pending" });
if (listResult.ok) {
console.log(`To-Do List:${listResult.value.length} items`);
listResult.value.forEach(t => console.log(` - ${t.title} [${t.priority}]`));
}
// Complete the task
if (addResult.ok) {
let doneResult = service.done(addResult.value.id);
if (doneResult.ok) {
console.log(`Completed:${doneResult.value.title}`);
}
}
Saída:
// Executed successfully
5. Camada de apresentação da CLI
// cli.ts
class TodoCLI {
constructor(private service: TodoService) {}
run(command: string, args: string[]): void {
switch (command) {
case "add": this.handleAdd(args); break;
case "list": this.handleList(args); break;
case "done": this.handleDone(args); break;
case "delete": this.handleDelete(args); break;
default: this.printHelp();
}
}
private handleAdd(args: string[]): void {
if (args.length === 0) {
console.log("Usage:add <Title> [Priority:low|medium|high]");
return;
}
let title = args[0];
let priority: Priority =
args[1] === "low" || args[1] === "medium" || args[1] === "high"
? args[1] : "medium";
let r = this.service.add({ title, priority });
if (r.ok) {
console.log(`✅ Added:${r.value.title} [${r.value.priority}]`);
} else {
console.log(`❌ Failed to add:${r.error}`);
}
}
private handleList(args: string[]): void {
let filter: TodoFilter = {};
if (args.includes("--done")) filter.status = "done";
if (args.includes("--pending")) filter.status = "pending";
let r = this.service.list(filter);
if (!r.ok) { console.log(`❌ Query Failed:${r.error}`); return; }
if (r.value.length === 0) { console.log("📭 No tasks"); return; }
console.log("\n📋 Task List:");
console.log("─".repeat(50));
for (let t of r.value) {
let s = t.status === "done" ? "✅" : "⬜";
let p = { low: "🟢", medium: "🟡", high: "🔴" }[t.priority];
console.log(`${s} ${p} [${t.id}] ${t.title}`);
}
console.log("─".repeat(50));
console.log(`Total ${r.value.length} task`);
}
private handleDone(args: string[]): void {
if (args.length === 0) { console.log("Usage:done <TaskID>"); return; }
let r = this.service.done(args[0]);
if (r.ok) console.log(`✅ Completed:${r.value.title}`);
else console.log(`❌ Operation Failed:${r.error}`);
}
private handleDelete(args: string[]): void {
if (args.length === 0) { console.log("Usage:delete <TaskID>"); return; }
let r = this.service.delete(args[0]);
if (r.ok && r.value) console.log("🗑️ Deleted");
else if (r.ok) console.log("❌ The task does not exist.");
else console.log(`❌ Deletion Failed:${r.error}`);
}
private printHelp(): void {
console.log("Todo CLI Tools:");
console.log(" add <Title> [Priority] Add a Task");
console.log(" list [--done|--pending] List Tasks");
console.log(" done <ID> Complete the task");
console.log(" delete <ID> Delete Task");
}
}
6. Arquivos de entrada
// index.ts
import { TodoService } from "./service";
import { JsonFileStorage } from "./storage";
import { TodoCLI } from "./cli";
import type { Todo } from "./types";
let storage = new JsonFileStorage<Todo>("./todos.json");
let service = new TodoService(storage);
let cli = new TodoCLI(service);
let args = process.argv.slice(2);
let command = args[0] ?? "list";
let commandArgs = args.slice(1);
cli.run(command, commandArgs);
7. Lista de conhecimentos do projeto
Os conceitos abordados neste projeto correspondem às seguintes aulas anteriores:
| Conceitos-chave | Curso correspondente | Aplicação em projetos |
|---|---|---|
| Tipos primitivos | Lição 4 | Tipos string e booleano do Todo |
| Inferência de tipos | Lição 5 | Anotações de tipos para variáveis omitidas |
| Tipos de união | Lição 6 | Prioridade, Status da tarefa |
| Matrizes | Lição 7 | Armazenando em Todo[] |
| Tipo de objeto | Lição 8 | Interfaces Todo e TodoFilter |
| Interface | Lição 9 | Definição da interface de armazenamento |
| Tipos de função | Lição 10 | Parâmetros de retorno de chamada de predicado |
| Aliases de tipos | Lição 11 | Tipo de resultado |
| Aula | Lição 13 | TodoService, JsonFileStorage |
| Classes e interfaces | Lição 14 | implementa Storage |
| Genéricos | Lição 15 | Adicionando parâmetros genéricos ao armazenamento |
| Restrições genéricas | Lição 16 | T estende { id: string } |
| Guardas de tipo | Lição 17 | Restrição de result.ok |
| Asserções de tipo | Lição 18 | Asserções “as T” |
| Tipo de ferramenta | Lição 19 | Omitir, Parcial, Gravar |
| Módulos | Lição 22 | Importação/Exportação de Módulos |
| tsconfig | Lição 24 | Configuração do projeto |
| Tratamento de erros | Lição 26 | Padrão de resultado |
▶ Exemplo: Implementação de armazenamento em memória
class MemoryStorage<T extends { id: string }> implements Storage<T> {
private data: Map<string, T> = new Map();
getAll(): Result<T[]> {
return ok(Array.from(this.data.values()));
}
getById(id: string): Result<T | null> {
return ok(this.data.get(id) ?? null);
}
save(item: T): Result<T> {
this.data.set(item.id, item);
return ok(item);
}
delete(id: string): Result<boolean> {
return ok(this.data.delete(id));
}
find(predicate: (item: T) => boolean): Result<T[]> {
let all = Array.from(this.data.values());
return ok(all.filter(predicate));
}
}
let memStore = new MemoryStorage<Todo>();
let memService = new TodoService(memStore);
let r = memService.add({ title: "Demo task", priority: "high" });
if (r.ok) console.log(`Added: ${r.value.title}`);
Saída:
Added: Demo task
▶ Exemplo: Arquitetura em camadas com injeção de dependência
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
class SilentLogger implements Logger {
log(_message: string): void { }
}
class App {
constructor(
private service: TodoService,
private logger: Logger
) {}
run(command: string): void {
this.logger.log(`Executing: ${command}`);
let result = this.service.list();
if (result.ok) {
this.logger.log(`Found ${result.value.length} items`);
}
}
}
let app = new App(
new TodoService(new MemoryStorage<Todo>()),
new ConsoleLogger()
);
app.run("list");
Saída:
[LOG] Executing: list
[LOG] Found 0 items
❓ Perguntas Frequentes
P: Quais recursos podem ser adicionados a este projeto? R: Sugestões de áreas para expansão: (1) Armazenamento assíncrono — mudar para
async/await(2) Sistema de tags — adicionar tagsstring[]às tarefas (3) Subtarefas — as tarefas podem ter subtarefas (4) Exportação para CSV/Markdown (5) Suporte a várias listas. Cada extensão é um ótimo exercício para reforçar o que você aprendeu nas lições anteriores.
P: Por que usar o padrão Result em vez de try/catch? R: Em ferramentas de CLI, os erros são erros de negócio previsíveis (como uma tarefa que não existe). O padrão Result deixa claro na assinatura da função que a operação “pode falhar” e que o chamador deve lidar com isso. Ele é mais adequado para esse cenário do que try/catch — o erro não é uma “exceção”, mas um “ramo normal”.
P: Por que a camada de armazenamento usa genéricos em vez de utilizar diretamente
Todo? R: Os genéricos tornam a camada de armazenamento reutilizável — ao adicionar parâmetros genéricos,JsonFileStoragepode armazenar qualquer entidade com uma propriedadeid. À medida que o projeto cresce, não há necessidade de escrever uma nova implementação de armazenamento para cada entidade. Esse é o valor central dos genéricos — escreva uma vez, use em qualquer lugar.
P: Como faço para converter um projeto em uma API da Web? R: A arquitetura central permanece inalterada — basta substituir a camada de CLI por uma camada de roteamento do Express. As camadas de armazenamento e de serviço são totalmente reutilizadas. Essa é a vantagem de uma arquitetura em camadas — a camada de apresentação pode ser substituída sem afetar a lógica de negócios.
📖 Resumo
- Comece pelo design tipográfico — defina os tipos principais, como
Todo,CreateTodo,UpdateTodoeResult - Derivar variantes do Todo utilizando tipos de ferramentas (Omit, Partial), seguindo o princípio DRY
- A camada de armazenamento genérica
Storage<T extends { id: string }>pode ser reutilizada para qualquer entidade - O padrão Result torna o tratamento de erros explícito — a assinatura da função declara que ela “pode falhar”, e quem a chama deve tratar o erro.
- Arquitetura em camadas: Camada de tipo → Camada de armazenamento → Camada de serviço → Camada de apresentação — cada camada tem responsabilidades claramente definidas e é independente das demais
- Integra a maioria dos conceitos abordados na Lição 29 — desde tipos básicos até genéricos e tratamento de erros
📝 Exercícios
- Exercício básico (Dificuldade: ⭐): Divida o código do projeto em arquivos .ts separados por módulo, configure o tsconfig.json e certifique-se de que
tscseja compilado com sucesso. - Exercício avançado (Dificuldade ⭐⭐): Adicione o recurso
tagao TodoService — adicione uma propriedadetags: string[]ao Todo para permitir a filtragem por tagslist --tag <labelName>. Você precisará modificar a definição do tipo, a camada de armazenamento e a camada da CLI. - Desafio (Dificuldade: ⭐⭐⭐): Converta o armazenamento síncrono em assíncrono — altere todos os métodos em
Storage<T>paraasynce retornePromise<Result<T>>. Além disso, converta as camadas de Serviço e CLI paraasync/await. Trate os erros assíncronos e garanta a segurança de tipos.