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

TEXT 📖 Somente leitura
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



2. Design tipográfico

(1) Definições de tipos de núcleo

TYPESCRIPT
// 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

TYPESCRIPT
// 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

TYPESCRIPT
// 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

TYPESCRIPT
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

TYPESCRIPT
// 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

TYPESCRIPT
// 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}`);
  }
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
// Executed successfully


5. Camada de apresentação da CLI

TYPESCRIPT
// 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

TYPESCRIPT
// 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

TYPESCRIPT
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}`);
▶ Experimente

Saída:

TEXT 📖 Somente leitura
Added: Demo task

▶ Exemplo: Arquitetura em camadas com injeção de dependência

TYPESCRIPT
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");
▶ Experimente

Saída:

TEXT 📖 Somente leitura
[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 tags string[] à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, JsonFileStorage pode armazenar qualquer entidade com uma propriedade id. À 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

📝 Exercícios

  1. 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 tsc seja compilado com sucesso.
  2. Exercício avançado (Dificuldade ⭐⭐): Adicione o recurso tag ao TodoService — adicione uma propriedade tags: string[] ao Todo para permitir a filtragem por tags list --tag <labelName>. Você precisará modificar a definição do tipo, a camada de armazenamento e a camada da CLI.
  3. Desafio (Dificuldade: ⭐⭐⭐): Converta o armazenamento síncrono em assíncrono — altere todos os métodos em Storage<T> para async e retorne Promise<Result<T>>. Além disso, converta as camadas de Serviço e CLI para async/await. Trate os erros assíncronos e garanta a segurança de tipos.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%