Design Patterns Tipados em TypeScript
Domine os 5 design patterns mais usados no TypeScript. Singleton, Factory, Observer, Strategy e Repository, todos tipados com exemplos de produção.
Por que isso é importante
Design Patterns Tipados em TypeScript. Domine os 5 design patterns mais usados no TypeScript. Singleton, Factory, Observer, Strategy e Repository, todos tipados com exemplos de produção.
Por Que Tipar Design Patterns
Design patterns resolvem problemas recorrentes de arquitetura. Mas em linguagens dinâmicas, a implementação depende da disciplina do dev. Nada impede alguém de passar um objeto errado pro Observer ou de instanciar o Singleton duas vezes.
Com TypeScript, os contratos do pattern ficam explícitos no código. Interfaces definem o que cada participante precisa implementar. Generics tornam os patterns reutilizáveis sem perder tipo. O compilador vira o fiscal que nunca dorme.
Vamos tipar os 5 patterns mais usados no dia a dia: Singleton, Factory, Observer, Strategy e Repository. Cada um com interface, implementação e exemplo de uso. Código que você copia e adapta pro seu projeto.
Os 5 Patterns Tipados Passo a Passo
Cada pattern segue a mesma estrutura: problema, interface, implementação tipada e uso.
Exemplos Práticos dos Design Patterns
Vamos pro código. Cada pattern com implementação completa e tipagem forte.
Singleton: Uma Instância, Tipo Garantido
class Database {
private static instance: Database | null = null;
private connections: number = 0;
// Construtor privado impede new Database()
private constructor(private readonly url: string) {}
static getInstance(url: string): Database {
if (!Database.instance) {
Database.instance = new Database(url);
}
return Database.instance;
}
connect(): void {
this.connections++;
console.log(`Conexão ${this.connections} em ${this.url}`);
}
getStatus(): { url: string; connections: number } {
return { url: this.url, connections: this.connections };
}
}
// Uso
const db1 = Database.getInstance("mongodb://localhost");
const db2 = Database.getInstance("mongodb://localhost");
console.log(db1 === db2); // true - mesma instância
// new Database("url"); // Error: Constructor is private
Factory: Criação Tipada de Objetos
// Interface do produto
interface Notificacao {
enviar(destino: string, mensagem: string): Promise<boolean>;
readonly tipo: string;
}
// Implementações concretas
class EmailNotificacao implements Notificacao {
readonly tipo = "email";
async enviar(destino: string, mensagem: string): Promise<boolean> {
console.log(`Email para ${destino}: ${mensagem}`);
return true;
}
}
class SmsNotificacao implements Notificacao {
readonly tipo = "sms";
async enviar(destino: string, mensagem: string): Promise<boolean> {
console.log(`SMS para ${destino}: ${mensagem}`);
return true;
}
}
class PushNotificacao implements Notificacao {
readonly tipo = "push";
async enviar(destino: string, mensagem: string): Promise<boolean> {
console.log(`Push para ${destino}: ${mensagem}`);
return true;
}
}
// Mapa de tipos pra factory
type TipoNotificacao = "email" | "sms" | "push";
const notificacaoMap: Record<TipoNotificacao, new () => Notificacao> = {
email: EmailNotificacao,
sms: SmsNotificacao,
push: PushNotificacao,
};
// Factory function tipada
function criarNotificacao(tipo: TipoNotificacao): Notificacao {
const Classe = notificacaoMap[tipo];
return new Classe();
}
// Uso
const notif = criarNotificacao("email");
await notif.enviar("joao@mail.com", "Olá!");
// criarNotificacao("whatsapp"); // Error: tipo inválido
Observer: Eventos com Payload Tipado
// Mapa de eventos tipados
interface EventMap {
"user:created": { id: number; name: string };
"user:deleted": { id: number };
"order:placed": { orderId: string; total: number };
}
// Observer genérico e tipado
class EventBus {
private listeners: {
[K in keyof EventMap]?: Array<(payload: EventMap[K]) => void>;
} = {};
on<K extends keyof EventMap>(
event: K,
callback: (payload: EventMap[K]) => void
): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
const callbacks = this.listeners[event];
if (callbacks) {
callbacks.forEach(cb => cb(payload));
}
}
off<K extends keyof EventMap>(
event: K,
callback: (payload: EventMap[K]) => void
): void {
const callbacks = this.listeners[event];
if (callbacks) {
this.listeners[event] = callbacks.filter(cb => cb !== callback);
}
}
}
// Uso - tudo tipado!
const bus = new EventBus();
bus.on("user:created", (payload) => {
// payload é { id: number; name: string } automaticamente
console.log(`Novo user: ${payload.name}`);
});
bus.emit("user:created", { id: 1, name: "Ana" }); // OK
// bus.emit("user:created", { id: 1 }); // Error: falta name
Strategy: Algoritmos Intercambiáveis
// Interface da strategy
interface PricingStrategy {
calcular(preco: number, quantidade: number): number;
readonly nome: string;
}
// Implementações concretas
class PrecoNormal implements PricingStrategy {
readonly nome = "normal";
calcular(preco: number, quantidade: number): number {
return preco * quantidade;
}
}
class PrecoBlackFriday implements PricingStrategy {
readonly nome = "black-friday";
calcular(preco: number, quantidade: number): number {
const desconto = quantidade >= 3 ? 0.3 : 0.1;
return preco * quantidade * (1 - desconto);
}
}
class PrecoVIP implements PricingStrategy {
readonly nome = "vip";
calcular(preco: number, quantidade: number): number {
return preco * quantidade * 0.8; // 20% off sempre
}
}
// Contexto que usa a strategy
class Carrinho {
private items: Array<{ nome: string; preco: number; qtd: number }> = [];
constructor(private strategy: PricingStrategy) {}
setStrategy(strategy: PricingStrategy): void {
this.strategy = strategy;
}
addItem(nome: string, preco: number, qtd: number): void {
this.items.push({ nome, preco, qtd });
}
getTotal(): number {
return this.items.reduce(
(total, item) => total + this.strategy.calcular(item.preco, item.qtd),
0
);
}
}
// Uso
const carrinho = new Carrinho(new PrecoNormal());
carrinho.addItem("Camiseta", 50, 2);
console.log(carrinho.getTotal()); // 100
carrinho.setStrategy(new PrecoBlackFriday());
console.log(carrinho.getTotal()); // 90 (10% off, menos de 3 itens)
Repository: CRUD Genérico Tipado
// Entidade base
interface Entity {
id: string;
createdAt: Date;
updatedAt: Date;
}
// Interface genérica do Repository
interface Repository<T extends Entity> {
findById(id: string): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T>;
update(id: string, data: Partial<Omit<T, "id">>): Promise<T | null>;
delete(id: string): Promise<boolean>;
}
// Entidade concreta
interface User extends Entity {
name: string;
email: string;
role: "admin" | "user";
}
// Implementação in-memory (teste/prototipagem)
class InMemoryUserRepo implements Repository<User> {
private users: User[] = [];
async findById(id: string): Promise<User | null> {
return this.users.find(u => u.id === id) ?? null;
}
async findAll(filter?: Partial<User>): Promise<User[]> {
if (!filter) return [...this.users];
return this.users.filter(u =>
Object.entries(filter).every(([k, v]) => u[k as keyof User] === v)
);
}
async create(data: Omit<User, "id" | "createdAt" | "updatedAt">): Promise<User> {
const user: User = {
...data,
id: crypto.randomUUID(),
createdAt: new Date(),
updatedAt: new Date(),
};
this.users.push(user);
return user;
}
async update(id: string, data: Partial<Omit<User, "id">>): Promise<User | null> {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return null;
this.users[index] = { ...this.users[index], ...data, updatedAt: new Date() };
return this.users[index];
}
async delete(id: string): Promise<boolean> {
const index = this.users.findIndex(u => u.id === id);
if (index === -1) return false;
this.users.splice(index, 1);
return true;
}
}
// Uso - o tipo User garante segurança total
const repo: Repository<User> = new InMemoryUserRepo();
const user = await repo.create({ name: "Ana", email: "ana@mail.com", role: "admin" });
// user.name é string, user.role é "admin" | "user" - tudo tipado
Cada pattern vira um contrato que o TypeScript fiscaliza. Factory retorna o tipo certo. Observer entrega o payload correto. Repository aceita só campos válidos. Sem surpresas em runtime.
Erros Comuns ao Tipar Design Patterns
Armadilhas que enfraquecem seus patterns
Singleton sem construtor privado: se o construtor é público, qualquer parte do código pode criar novas instâncias. O pattern perde o sentido. Use private constructor obrigatoriamente.
Factory que retorna any: uma Factory que retorna any em vez do tipo específico elimina toda a vantagem da tipagem. Use interfaces e generics pra garantir o tipo de retorno.
Observer sem EventMap: se o sistema de eventos aceita qualquer string como nome e any como payload, você tá de volta ao JavaScript. Defina um mapa de eventos tipado.
Strategy com implementação incompleta: se uma implementação não segue todos os métodos da interface, o TypeScript acusa erro. Não use // @ts-ignore pra contornar. Implemente tudo.
Repository sem Omit nos campos automáticos: se o método create aceita o tipo completo incluindo id e createdAt, quem chama precisa inventar esses valores. Use Omit pra excluir campos que são gerados internamente.
Checklist de Design Patterns Tipados
TypeScript Avançado na Prática
Design patterns tipados são o que separa código amador de código profissional. Mas a mágica acontece quando você aplica Singleton, Factory, Observer, Strategy e Repository juntos num projeto real com camadas bem definidas. No CrazyStack, você constrói um SaaS completo com Node.js, React e TypeScript, aplicando cada pattern no contexto certo.
Se você quer sair da teoria e ver esses patterns funcionando em produção com tipagem de ponta a ponta, esse é o próximo passo.
Continue lendo
Como Usar Generics no TypeScript
Crie funções e tipos reutilizáveis com generics no TypeScript.
Como Usar Discriminated Unions no TypeScript
Modele estados complexos com uniões discriminadas e exhaustive checks.
Como Tipar Promise e Async/Await no TypeScript
Tipagem segura de código assíncrono com Promise, async/await e error handling.
Interface no TypeScript
Quando usar interface