Sabe aquela sensação de pisar em ovos toda vez que você precisa mexer em uma regra de negócio complexa, principalmente na UI?
Seja lidando com status de pedidos, gateways de pagamento ou níveis de permissão, manter a consistência do código muitas vezes parece caminhar por um campo minado.
A reação natural de muitos devs (e isso me lembra muito o meu começo :D) é jogar essas variações em um switch ou em uma sequência infinita de if/else. O problema é que basta esquecer um único case ou errar um encadeamento e pronto: você tem um bug silencioso rodando em produção.
Muitos tentam resolver isso aplicando Guard Clauses (o famoso "return early"). É uma técnica excelente para limpar validações e evitar o aninhamento excessivo. Aliás, esse é um tema que rende um post dedicado só para ele no futuro.
Mas a proposta hoje vai além. Não quero apenas organizar os ifs, eu quero eliminar eles.
A meta é parar de escrever condicionais defensivas e começar a escrever intenção.
Quando lidamos com regras previsíveis, Object Maps combinados com Mapped Types oferecem uma abordagem muito mais robusta. A grande sacada é transformar a fragilidade das condicionais na segurança de um contrato.
👇 Como eu aplico isso na prática:
Em vez de espalhar lógica de controle imperativa, eu crio uma estrutura de dados declarativa e exaustiva.
📱 O Caso Real (React Native + Zustand)
Imagine um app de delivery onde cada status de pedido define: texto, cor, permissão de cancelamento e ação ao clicar.
Sem Object Map, isso vira lógica espalhada pela UI. Com o padrão, começamos definindo o domínio:
export type OrderStatus =
| 'pending' | 'confirmed' | 'preparing' | 'delivered' | 'cancelled';
type OrderStatusConfig = {
label: string;
canCancel: boolean;
// Note que tirei a cor daqui. Já explico o porquê! 👇
}
Muitos colocariam um switch aqui. Mas vamos usar a tipagem a nosso favor com a sintaxe [K in Tipo]:
// O compilador obriga que TODAS as chaves de OrderStatus existam aqui.
type OrderStatusMap = {
[K in OrderStatus]: OrderStatusConfig
}
export const orderStatusMap: OrderStatusMap = {
pending: { label: 'orderStatus.pending', canCancel: true },
confirmed: { label: 'orderStatus.confirmed', canCancel: true },
preparing: { label: 'orderStatus.preparing', canCancel: false },
delivered: { label: 'orderStatus.delivered', canCancel: false },
cancelled: { label: 'orderStatus.cancelled', canCancel: false },
};
Se adicionar um novo status e esquecer de mapear, o TS quebra o build.
🔥 Uma abordagem mais pragmática
Se você quiser inferência de tipos mais precisa sem perder a checagem, o operador satisfies é perfeito. Ele valida se o objeto cumpre o contrato, mas mantém a tipagem literal dos valores.
export const orderStatusMap = {
pending: { ... },
confirmed: { ... },
// ...restante
} satisfies Record<OrderStatus, OrderStatusConfig>
🎨 Separação de Responsabilidades (Domain vs UI)
Aqui está o "pulo do gato" para arquiteturas limpas. O seu mapa de regras (orderStatusMap) não precisa conhecer o seu Design System. Podemos criar um segundo mapa com responsabilidade única de vincular status ao tema:
// Vinculando chaves do status às chaves do seu tema
// (Ex: NativeBase, Restyle, etc)
export const orderStatusColorMap = {
pending: 'warning',
confirmed: 'info',
preparing: 'orange',
delivered: 'success',
cancelled: 'danger',
} satisfies Record<OrderStatus, keyof ThemeColors>
🚀 Consumindo na UI (Zero Ifs)
No componente React Native, a lógica desaparece. Tudo vira acesso direto:
// Zustand
import { create } from 'zustand'
type OrderStore = {
status: OrderStatus
setStatus: (status: OrderStatus) => void
}
export const useOrderStore = create<OrderStore>(set => ({
status: 'pending',
setStatus: status => set({ status }),
}))
export function OrderStatusView() {
const { t } = useTranslation();
const status = useOrderStore(state => state.status);
const theme = useTheme();
// Desestruturação direta da regra de negócio
const { label, canCancel } = orderStatusMap[status];
// Resolução da cor via Design System
const color = theme.colors[orderStatusColorMap[status]];
return (
<>
<Text style={{ color }}>{t(label)}</Text>
{canCancel && (
<Pressable onPress={() => console.log('Cancelar')}>
<Text>Cancelar pedido</Text>
</Pressable>
)}
</>
)
}
🧠 Indo além
Esse padrão não é só para UI. Imagine lidar com PIX e Cartão processados por gateways diferentes (Stripe, Mercado Pago).
Em vez de condicionais aninhadas, usamos um Object Map que retorna a estratégia correta de execução:
export const paymentStrategies = {
pix: async (data, gateway) => {
return adaptersMap[gateway].pix(data)
},
credit_card: async (data, gateway) => {
return adaptersMap[gateway].creditCard(data)
},
} satisfies Record<PaymentMethod, PaymentHandler>
// Uso na aplicação:
// A lógica "escolhe" a função correta e já executa.
const result = await paymentStrategies[method](negotiation, selectedGateway);
Por que isso muda o jogo?
- Complexidade O(1): Removemos o custo cognitivo de ler múltiplos
elses. - Paz de espírito: Se eu criar um novo status ou método de pagamento, não preciso caçar onde colocar o
if. O compilador me guia apontando o erro. - Contratos Claros: O código deixa de ser defensivo e passa a expressar intenção.
E você, costuma usar Object Maps ou ainda confia na memória para manter seus switch cases atualizados? 👇
Top comments (0)