Padrões Arquiteturais em TypeScript: como parar de escrever "espaguete" e começar a viver
\n\nVocê já abriu um projeto em TypeScript, escrito há seis meses, e percebeu com horror que não conseguia entender suas próprias classes? Ou talvez você estava adicionando um novo recurso e a aplicação quebrava em três lugares inesperados? Familiar? Bem-vindo ao clube dos desenvolvedores que um dia decidiram que "está bom assim" — e pagaram por isso.
\n\nTypeScript não é apenas uma extensão do JavaScript com tipos. É uma ferramenta poderosa que permite construir sistemas complexos, escaláveis e sustentáveis. Mas os tipos por si só não vão salvá-lo do caos arquitetural. Você precisa de padrões arquiteturais — receitas testadas pelo tempo que ajudam a organizar o código para que ele seja compreensível, testável e flexível.
\n\nNeste artigo, vamos analisar cinco padrões arquiteturais TypeScript principais, que são mais frequentemente usados em projetos reais. Você verá exemplos de código, entenderá onde e por que aplicar cada padrão e poderá implementá-los imediatamente no seu trabalho. Vamos lá!
\n\n1. MVC (Model-View-Controller) — o clássico do gênero
\n\nVamos começar com o padrão mais conhecido. MVC em TypeScript é a divisão da aplicação em três camadas:
\n\n- \n
- Model — dados e lógica de negócio. \n
- View — exibição (UI). \n
- Controller — elo de ligação, processa a entrada do usuário. \n
Em aplicações TypeScript, o MVC é especialmente bom porque podemos tipificar estritamente todas as camadas, eliminando erros em tempo de compilação.
\n\nExemplo: uma lista de tarefas simples (Todo List)
\n\n// Model\ninterface Task {\n id: number;\n title: string;\n completed: boolean;\n}\n\nclass TaskModel {\n private tasks: Task[] = [];\n\n addTask(title: string): Task {\n const newTask: Task = {\n id: Date.now(),\n title,\n completed: false,\n };\n this.tasks.push(newTask);\n return newTask;\n }\n\n getTasks(): Task[] {\n return this.tasks;\n }\n}\n\n// View\nclass TaskView {\n render(tasks: Task[]): void {\n console.clear();\n tasks.forEach(task => {\n console.log(`${task.id}: [${task.completed ? 'x' : ' '}] ${task.title}`);\n });\n }\n}\n\n// Controller\nclass TaskController {\n constructor(\n private model: TaskModel,\n private view: TaskView\n ) {}\n\n addTask(title: string): void {\n this.model.addTask(title);\n this.view.render(this.model.getTasks());\n }\n\n showAll(): void {\n this.view.render(this.model.getTasks());\n }\n}\n\n// Uso\nconst model = new TaskModel();\nconst view = new TaskView();\nconst controller = new TaskController(model, view);\ncontroller.addTask('Estudar padrões TypeScript');\ncontroller.addTask('Escrever artigo');\n\nPor que isso é legal? Você pode mudar a View (por exemplo, migrar do console para React) sem tocar no Model. Pode testar a lógica de negócio separadamente da UI. A arquitetura da aplicação TypeScript torna-se transparente.
\n\n2. Observer — o rei dos eventos
\n\nSe sua aplicação vive de eventos (cliques, respostas do servidor, temporizadores), o padrão Observer TypeScript é seu melhor amigo. Ele permite que objetos (assinantes) acompanhem as mudanças de outro objeto (publicador).
\n\nExemplo: sistema de notificações
\n\n// Interface do observador\ninterface Observer {\n update(data: string): void;\n}\n\n// Publicador\nclass NewsPublisher {\n private observers: Observer[] = [];\n\n subscribe(observer: Observer): void {\n this.observers.push(observer);\n }\n\n unsubscribe(observer: Observer): void {\n this.observers = this.observers.filter(obs => obs !== observer);\n }\n\n publishNews(news: string): void {\n this.observers.forEach(obs => obs.update(news));\n }\n}\n\n// Observador concreto: Notificação por Email\nclass Em