Kotlin Christmas

Sealed classes

A 2 minute read written by
Henrik Gundersen
05.12.2019

Previous postNext post

Sealed classes in Kotlin are a concept close to enums that we are used to from other languages. While enums are used to represent a set of values — sealed classes are used for representing restricted class hierarchies. This means that each type have to be within a limited set of values and can have several instances containing state. This gives us the possibility to make more expressive data models.

Declaring sealed classes

To declare a sealed class — put the sealed modifier in front of the class name. Note that a sealed class can have subclasses, but all of them must be declared in the same file as the sealed class itself.

To illustrate this, let's look into a simple example. In this example we are doing a payment with different payment methods:

sealed class PaymentMethod() {
  data class CreditCard(val token: String, val expireDate: String) : PaymentMethod()
  data class DirectPayment(val phoneNumber: String) : PaymentMethod()
}

Each of these payment methods have different requirements for input data. When handling a CreditCard payment we need a token and expireDate. A DirectPayment however only requires a phoneNumber.

As we can see here, each of the values have to extend the sealed class PaymentMethod. Every type defined inside PaymentMethod, might have their own state, extend state from PaymentMethod or they can even be an object with no state at all.

Utilizing the when expression

The real power of sealed classes can be seen using the when expression. As described earlier, there is a restricted set of possible types when defining sealed classes. This restriction makes it possible for the compiler to warn you if you are not handling all possible outcomes in the when expression.

In the following example we are trying to process a payment:

data class Payment(amount: Int, paymentMethod: PaymentMethod)

// Does not compile :(
fun processPayment(payment: Payment) =
  when (payment.paymentMethod) {
    is PaymentMethod.CreditCard -> processCreditCardPayment(payment.amount, paymentMethod.token, paymentMethod.expireDated)
}

This code will not compile. Can you see what's missing in the processPayment function?🤔

As you probably could see, we didn't handle all possible outcomes of PaymentMethod. Let's add the missing type DirectPayment to the processPayment method:

data class Payment(amount: Int, paymentMethod: PaymentMethod)

// Compiles :)
fun processPayment(payment: Payment) =
  when (payment.paymentMethod) {
    is PaymentMethod.CreditCard -> 
      processCreditCardPayment(payment.amount, paymentMethod.token, paymentMethod.expireDated)
    is PaymentMethod.DirectPayment ->
      processDirectPayment(payment.amount, paymentMethod.phoneNumber)
}

The above code will now compile since all possible values of PaymentMethod is handled inside the when expression.

Another important benefit we get from using when expressions, is smartcast. When the paymentMethod matches one of the is statements - we can use the object directly without the need for explicit casting.

Summary

Sealed classes gives us a new tool for creating cleaner and better data models. By utilizing sealed classes with the when expression, the compiler will also help us to handle all possible values. Even though this sometimes might feel cumbersome, it will definitely make the code more solid and more maintainable.

Read the next post

Read more outside the calendar