Trilha de Kotlin · parte 13 de 59
sealed e when exaustivo
Aula da fase 2 da trilha de Kotlin: Como modelar estados de um processo para que o compilador avise quando um caso novo não foi tratado?
intermediárioPublicada em · 11 min de leitura
sealed e when exaustivo
Pense num semáforo. Ele tem três cores, e um motorista que vê uma cor sabe exatamente o que fazer. Agora imagine que a prefeitura acrescente uma quarta luz, azul, para pedestres, e que nenhum motorista seja avisado. O sistema continua funcionando, mas cada decisão tomada com base na cor antiga passa a ser incompleta, e o problema só aparece quando alguém não sabe o que fazer no cruzamento.
Em código, esse cruzamento é um if ou um switch que trata os casos conhecidos e deixa os demais de lado, sem que ninguém perceba. Quando um estado novo é adicionado ao modelo, o código antigo continua compilando, e o erro vira um comportamento estranho em produção. A aula trata de uma ferramenta do Kotlin que faz o compilador apontar esses cruzamentos: a sealed define um conjunto fechado de possibilidades, e o when exaustivo exige que todas sejam tratadas.
A trilha pede uma resposta específica: como modelar os estados de um processo para que o compilador avise quando um caso novo não foi tratado. Vamos usar como exemplo o ciclo de um pagamento, que é um bom caso porque cada estado carrega dados diferentes e a transição entre eles precisa ser pensada com cuidado.
1. o problema de representar estados com texto ou com enum simples
Antes da solução, vale ver os jeitos comuns de representar estados. Um deles é usar String, como "PENDENTE" ou "APROVADO", guardado num campo. Isso falha de três formas. Um typo como "APROVDO" compila e só aparece no banco. Nenhum tipo impede que o código trate o status como texto livre. E a lista de estados possíveis fica espalhada em comentários e em testes.
Um enum resolve os typos, porque o compilador conhece todas as constantes. Mas um enum tem um limite claro: todas as constantes têm o mesmo formato. Num pagamento, o estado Recusado precisa de um motivo, e o estado Aprovado precisa de um código de autorização. Com enum, esses dados viram campos opcionais no objeto de pagamento, e o modelo passa a permitir combinações impossíveis, como um pagamento aprovado sem autorização, ou um pagamento pendente com motivo de recusa. O compilador não tem como saber que esses campos só fazem sentido em certos estados.
A combinação de String, enum e campos nuláveis produz o que o time costuma chamar de modelo frouxo. Ele aceita tudo, e a regra de que certos dados só existem em certos estados fica escrita apenas na cabeça de quem programou. Quando o código cresce, essa regra se perde.
2. sealed: um conjunto fechado de subtipos
Uma sealed interface ou uma sealed class declara um tipo cujas implementações diretas são conhecidas em tempo de compilação. Cada estado vira uma classe própria, com os dados que só aquele estado precisa. Assim, o motivo de recusa existe apenas dentro de Recusado, e não há campo opcional para preencher no estado errado.
sealed interface Pagamento
data class Pendente(val pedido: String) : Pagamento
data class Aprovado(val pedido: String, val autorizacao: String) : Pagamento
data class Recusado(val pedido: String, val motivo: String) : Pagamento
data object Estornado : Pagamento
A documentação oficial coloca duas regras de ouro. As subclasses diretas precisam estar no mesmo pacote da declaração selada, e a documentação diz que nenhuma subclasse pode aparecer fora do módulo e do pacote onde a classe selada foi definida. Isso faz o compilador enxergar o conjunto inteiro. E depois que o módulo é compilado, nenhuma nova implementação direta pode aparecer por fora. O conjunto é fechado de verdade, e isso é o que permite a verificação de exaustividade. Uma implementação em outro módulo, por exemplo, é recusada pelo compilador.
O data object Estornado aparece porque esse estado não carrega dados. Um object comum serviria, mas o data object ganha toString com o nome da constante, o que ajuda no log. Ele está disponível a partir do Kotlin 1.9.
Repare que cada estado é uma classe com seus próprios campos. Isso muda a pergunta que o código faz. Em vez de perguntar “o status é recusado, então onde está o motivo?”, o código recebe um Recusado e já tem o motivo em mãos. Essa é a parte da modelagem que a trilha chama de tornar estados inválidos impossíveis de expressar.
3. when exaustivo: o compilador confere os casos
Um modelo fechado só vale se o código que o usa cobrir todos os casos. É aqui que entra o when. Quando o assunto é um tipo selado, o compilador exige que cada subtipo tenha um ramo, e não precisa de else:
fun rotular(p: Pagamento): String = when (p) {
is Pendente -> "aguardando ${p.pedido}"
is Aprovado -> "aprovado, autorização ${p.autorizacao}"
is Recusado -> "recusado: ${p.motivo}"
Estornado -> "estornado"
}
Repare que dentro de cada ramo o compilador sabe o tipo exato, então p.motivo só existe no ramo de Recusado. Isso é o smart cast funcionando junto com a sealed. A saída do teste foi:
aguardando P1
aprovado, autorização A99
recusado: saldo insuficiente
estornado
Agora imagine que a equipe acrescenta um estado Cancelado e esquece de atualizar o when. O compilador responde com:
'when' expression must be exhaustive. Add the 'is Cancelado' branch or an 'else' branch.
O erro aponta o nome exato do caso que falta, e o código não passa do build. Esse é o ganho central da aula. A mudança no modelo e o lugar onde ela precisa ser tratada ficam ligados pelo compilador, e ninguém depende da memória de quem escreveu a primeira versão.
Vale notar que a exaustividade vale também para when usado como instrução, sem devolver valor. Até o Kotlin 1.6, um when como instrução sobre um tipo selado podia ficar incompleto com apenas um aviso. O guia de compatibilidade do Kotlin 1.7 registra que esse aviso passou a ser erro, e que o mesmo vale para enum e para Boolean. Hoje, usar when sobre um tipo selado sempre exige cobrir os casos.
O diagrama mostra os estados como um grupo fechado, que é exatamente o que o compilador enxerga. As setas são as transições: aprovar(), recusar() e estornar(). O estado Recusado aparece em rosa porque é o único terminal de falha, e a relação entre o grupo e os estados lembra que o conjunto é fechado.
Há uma limitação que o diagrama não mostra e que você precisa conhecer. A sealed garante que todos os estados existem e que o when trata todos eles. Ela não garante que uma transição seja válida. Nada no código acima impede que Estornado vire Aprovado por uma atribuição qualquer, se o campo for var. Para as transições, a regra precisa ficar numa função de domínio que recebe o estado atual e devolve o novo, e se o estado for imutável, a transição válida se torna a única forma de produzir um estado novo.
4. quando o else atrapalha
O else é a forma mais comum de silenciar o compilador, e ele tem um custo que costuma passar despercebido. Quando você escreve else -> ..., qualquer caso futuro cai no else sem aviso. A exaustividade continua valendo para o que existe, mas o novo caso não gera erro nenhum.
Compare com um when sobre uma interface comum, que não é selada. Como o compilador não conhece todas as implementações, ele exige um else:
interface Evento
class Criado : Evento
fun descrever(e: Evento): String = when (e) {
is Criado -> "criado"
}
O compilador recusa o código com 'when' expression must be exhaustive. Add an 'else' branch. Ou seja, a falta de sealed obriga o else, e o else apaga a proteção. Com um else -> "outro" no lugar, o código compila e um Pago novo passaria a ser descrito como "outro" sem nenhum sinal.
A conclusão é que, num domínio que você controla, o else deve ser exceção, e não reflexo. Use else quando o conjunto de casos realmente é aberto, como a classificação de um código de erro de uma API externa que pode crescer sem aviso. Dentro do seu modelo fechado, deixe o compilador cobrar.
Vale mencionar que o Java chegou a um lugar parecido. Sealed classes entraram no Java e, com o Java 21, o switch com padrões foi finalizado. A ideia é a mesma, e um switch sobre tipos selados pode ser exaustivo. A diferença é que, no Kotlin, a sealed existe há mais tempo e o when aparece em lugares onde o Java ainda usa cadeias de instanceof.
O diagrama compara os dois caminhos lado a lado. À esquerda, o when sem else falha no momento em que o caso Cancelado aparece, e a seta com o X mostra o bloqueio. À direita, o mesmo caso cai no else e compila em silêncio. Os dois grupos têm a mesma estrutura de propósito, para que a diferença esteja só no ramo final.
Pergunta de entrevistaPor que sealed com when é mais seguro que enum com else?
Com um enum e um else, um caso novo pode cair no else sem nenhum aviso. Com sealed, os subtipos são conhecidos e o compilador exige que o when cubra todos. Quando adiciono um estado, o build quebra nos pontos que ainda não tratam o novo caso. Assim o else fica só para o que é realmente padrão, e não para esconder casos futuros. Além disso, cada estado pode carregar os próprios dados.
5. erros comuns e boas práticas
O erro mais frequente é colocar else em todo when por hábito. Ele faz o código compilar sempre, e é justamente por isso que anula a verificação. Antes de escrever else, pergunte se o conjunto de casos é fechado. Se for, deixe o compilador listar os ramos.
O segundo erro é modelar o estado com enum quando cada estado tem dados próprios. Você acaba com campos opcionais e combinações inválidas, que o enum não impede. Quando os estados carregam dados diferentes, use sealed.
O terceiro erro é achar que a sealed garante o ciclo de vida inteiro. Ela garante os estados e a cobertura dos casos, mas não as transições. Coloque as transições em funções que recebem o estado atual e devolvem o novo, e prefira estados imutáveis, para que o caminho válido seja o único que o código consegue percorrer.
O quarto erro é deixar uma sealed com estados demais em um único when espalhado pelo código. Quando um tipo tem muitas utilizações, a mudança de um estado acaba tocando dezenas de arquivos, o que é o custo de um modelo que o compilador protege. Esse custo é aceitável, e até desejável, porque cada ponto alterado é um ponto que o compilador apontou.
6. exercícios
Modele um chamado de suporte com os estados Aberto, EmAtendimento(responsavel), Resolvido(solucao) e Fechado. Escreva uma função resumir com when sem else e confirme que acrescentar um estado Reaberto produz erro de compilação no lugar certo.
Escreva uma função podeEstornar(p: Pagamento): Boolean que devolve verdadeiro apenas para Aprovado. Use when como expressão e verifique que o compilador exige todos os ramos.
Pegue o when da aula com else e remova o else. Depois adicione o estado Cancelado e compare o comportamento antes e depois, anotando qual erro cada versão produz.
7. glossário
Sealed (selado): tipo cujas implementações diretas são conhecidas em tempo de compilação e precisam estar no mesmo pacote e módulo da declaração.
Exaustividade: propriedade de um when que cobre todos os casos possíveis de um tipo, de forma que nenhum valor fique sem ramo.
data object: objeto com nome legível em toString, usado para estados sem dados. Está disponível a partir do Kotlin 1.9.
Smart cast: conversão automática de tipo dentro de um ramo que testou o tipo com is, permitindo acessar os campos específicos daquele subtipo.
Estado terminal: estado do qual nenhuma transição sai, como Estornado no modelo desta aula.
Fontes
Consultadas em 20/10/2026.
- Documentação oficial de sealed classes e interfaces, com as regras de pacote e módulo e o uso do
when: https://kotlinlang.org/docs/sealed-classes.html - Guia de compatibilidade do Kotlin 1.7.0, com a mudança que transforma
whennão exaustivo sobre tipos selados, enums e Boolean em erro: https://kotlinlang.org/docs/compatibility-guide-17.html - Versão estável listada na documentação oficial de releases: 2.4.21, de 8 de outubro de 2026: https://kotlinlang.org/docs/releases.html
Os exemplos foram compilados e executados com o kotlinc 2.1.21. As mensagens de erro citadas na aula são as que esse compilador produziu. A referência ao Java 21 foi conferida na JEP 441, que finalizou o switch com padrões nessa versão e exige exaustividade nesse tipo de switch.
Relacionados
- Trilha de Kotlin
- Curriculo de um Desenvolvedor Kotlin
- Null safety e tipos nuláveis
- data class, enum e value class
- Funções de escopo e funções de extensão