skip to content

Sealed Types

Sealed classes and interfaces close a hierarchy at compile time, so the compiler can check that you handled every case. They are the Kotlin answer to algebraic data types and the backbone of result and state modeling.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What is a sealed class (or sealed interface) in Kotlin, and what does the sealed modifier guarantee about its subtypes?

level: juniorimportance: must knowfreq 80%

answer

  1. Closed set of subtypes, known at compile time
  2. sealed class is implicitly abstract — instantiate subtypes
  3. Outside code cannot add new subtypes
  4. Subtypes can be data class / object / sealed
  5. Enables exhaustive when without else

basics

~20 s

A sealed class or interface is one where the compiler knows the complete, fixed list of all its direct subtypes. New subtypes can't be added from outside, so the set of possibilities is closed and known at compile time.

solid answer

~40 s

The sealed modifier marks a class or interface as a restricted hierarchy: the compiler knows every direct subtype at compile time, and no outside code can add more. A sealed class is also implicitly abstract, so you cannot instantiate it directly; you instantiate one of its subclasses. Subtypes must live in the same module and same package as the sealed declaration (the permitted-subtype rule). Sealed interfaces work the same way but, being interfaces, allow a type to implement several of them. The closed-set guarantee is what makes a when over the type exhaustive without an else branch. Typical subtypes are data classes, objects, or further sealed types. Compared to an enum, each subtype can hold its own distinct state and you can have multiple instances of a subtype.

code

kotlin · 8 lines
kotlin
sealed class PaymentMethod {
    data class Card(val last4: String) : PaymentMethod()
    data class Bank(val iban: String) : PaymentMethod()
    object Cash : PaymentMethod()
}

// PaymentMethod() // compile error: cannot create instance of sealed class
val pm: PaymentMethod = PaymentMethod.Cash

go deeper

for a junior

Can define a sealed class with a couple of subtypes and explain the closed-set idea in plain words.

for a middle

Knows it's implicitly abstract, distinguishes sealed class vs sealed interface, and connects the closed set to compiler exhaustiveness.

for a senior

Frames sealed as a restricted hierarchy used to model fixed alternatives and contrasts it precisely with open hierarchies and enums.

for a principal

Articulates the design intent — encoding invariants in the type system so illegal states are unrepresentable and changes are compiler-verified.

## What 'sealed' means The `sealed` keyword is a class/interface modifier that creates a **closed (restricted) type hierarchy**. 'Closed' means the compiler can see the *complete* set of direct subtypes at compile time, and nothing outside the declaration's allowed location can introduce new ones. ```kotlin sealed interface Shape data class Circle(val radius: Double) : Shape data class Rectangle(val width: Double, val height: Double) : Shape object Unknown : Shape ``` Here `Shape` has exactly three direct subtypes. No other file or library can add a fourth implementer. ## Key properties - **Implicitly abstract (for sealed *class*):** A `sealed class` cannot be instantiated directly — `Shape()` would not compile if `Shape` were a class. You create instances of its subtypes instead. Its constructors are effectively `protected` by default. - **A sealed *interface* is just an interface** with a closed implementer set. Because interfaces support multiple inheritance, one type can implement several sealed interfaces. - **Compile-time-known subtype set:** Because the set is fixed, the compiler can reason about completeness — this is what enables an exhaustive `when` without an `else`. - **Subtypes are normal types:** They can be `data class`, `object`, regular `class`, or even another `sealed` type, and each can carry its own state and behavior. ## Why use it Sealed types model 'one of a fixed set of things' — a value is *exactly one* of the known subtypes. This gives you type-safe alternatives that the compiler can check, unlike an open class hierarchy where any subtype could appear. ## Sealed vs open A plain `open class` or ordinary `interface` is **open**: any number of unknown subtypes can exist anywhere. `sealed` deliberately removes that openness to gain compile-time knowledge of the whole hierarchy.

  • Can you instantiate a sealed class directly?
    No. A sealed class is implicitly abstract; you instantiate one of its subtypes. A sealed interface also can't be instantiated since interfaces never can.
  • Does a sealed type prevent subclassing entirely?
    No — it restricts *who* can subclass and *where*. Subtypes are still allowed, but only within the same module and package as the sealed declaration.

Like a multiple-choice question with a fixed list of answers — the answer must be one of the printed options, and nobody can scribble in a new one.

saying these in an interview costs you the question

  • Saying a sealed class has no subtypes / can't be extended at all
  • Claiming you can instantiate a sealed class directly with its constructor
  • Thinking sealed means the same as final
  • Confusing sealed with enum (only one value per case)

context

open as a page

When you use `when` as an expression over a sealed type and cover every subtype, why don't you need an `else` branch?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Because the sealed type lists all its possible subtypes in advance. If you handle each one, the compiler knows nothing else is possible, so an else branch would be pointless.

open as a page

You need to represent a screen that can be loading, show data, or show an error. How would you model this with a sealed type, and why is that better than three nullable fields?

level: juniorimportance: must knowfreq 80%

basics

~10 s

Make a sealed type with three cases: Loading, Success holding the data, and Error holding a message. Only one case can exist at a time, so you can never have a half-loaded, half-errored screen.

open as a page

What is the core difference between a Kotlin enum class and a sealed class/interface, and when would you reach for each?

level: juniorimportance: must knowfreq 80%

basics

~20 s

An enum is a fixed set of named constant objects that all look the same. A sealed type is a closed set of subclasses that can each have different fields and many instances. Use enum for simple constants, sealed for varied data.

open as a page

Where are subtypes of a sealed class or interface allowed to be declared? Explain the same-module / same-package permitted-subtype rule.

level: middleimportance: must knowfreq 65%

basics

~20 s

Direct subtypes must be in the same module as the sealed declaration, and in the same package. You can nest them inside the sealed type or put them in separate top-level files, as long as they stay in that module and package.

open as a page

In a `when` over a sealed type, how does smart-casting work inside each `is` branch, and what can prevent it?

level: middleimportance: must knowfreq 60%

basics

~20 s

Inside an is Subtype -> branch the compiler already knows the value is that subtype, so it auto-casts it and you can use the subtype's properties directly. A mutable var that could change can block this.

open as a page

In a sealed state type, when should a case be a `data object` versus a `data class` with no meaningful fields, and how does that choice affect equality and StateFlow emissions?

level: middleimportance: must knowfreq 55%

basics

~20 s

If a state carries no data (like Loading), make it a single shared object. If it carries data, make it a data class. A shared object is always equal to itself, which keeps state comparisons and StateFlow updates predictable.

open as a page

When would you choose a sealed interface over a sealed class, and what capabilities differ between them?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use a sealed interface when a type may need to belong to several closed hierarchies at once, since a class can implement many interfaces but extend only one class. Use a sealed class when you want shared state (stored properties) or a single constructor in the parent.

open as a page

What is the difference between `when` used as a statement vs an expression over a sealed type with respect to exhaustiveness, and how did Kotlin 1.7 change this?

level: middleimportance: should knowfreq 45%

basics

~20 s

As an expression (its value is used), when over a sealed type must cover all subtypes. As a statement (value ignored), older Kotlin didn't check this; since 1.7 it does and warns/errors if a case is missing.

open as a page

Implement a reusable generic Result type as a sealed hierarchy with Success<T> and Error cases. How do generics interact with object subtypes, and how would you write a `map` that transforms the success value?

level: middleimportance: should knowfreq 70%

basics

~10 s

Make a sealed type Result<T> with Success holding a T and Error holding a throwable. Add a map function that, if it's Success, transforms the value, and if it's Error, passes it through unchanged.

open as a page

You have an enum where entries are sprouting nullable fields to hold case-specific data. How do you convert it to a sealed hierarchy, and what do you gain and lose?

level: middleimportance: should knowfreq 60%

basics

~20 s

Turn each enum entry into its own subclass of a sealed type. Cases that carry data become data classes; data-free cases become objects. You gain per-case fields and type-safe access; you lose enum freebies like ordinal, valueOf, and entries.

open as a page

In a sealed hierarchy, when should a case be an object/data object versus a data class, and how does that map back to enum entries?

level: middleimportance: should knowfreq 45%

basics

~20 s

Use an object (or data object) when the case carries no data — it's a singleton like an enum entry. Use a data class when the case needs its own fields and may have many instances. data object adds a clean toString and equals.

open as a page

How are sealed classes compiled on the JVM, and what are the rules around their constructors and instantiation?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A sealed class compiles to an abstract class whose constructors are not accessible from outside, so it can't be subclassed or instantiated externally. Reflection can list its subclasses via sealedSubclasses, and the compiler tracks the permitted set.

open as a page

How do exhaustiveness and smart-casting behave for a `when` over a nullable sealed type, and over a sealed hierarchy that itself nests sealed subtypes?

level: seniorimportance: should knowfreq 35%

basics

~20 s

If the value can be null you must add a null branch (or handle it before) to stay exhaustive. For nested sealed types you can either list each leaf or match the intermediate sealed parent in one branch — the compiler tracks both.

open as a page

Designers want the list screen to keep showing old data while a refresh runs, and to show a refresh error as a banner without hiding the data. A flat sealed Loading/Success/Error model can't express 'data + refreshing' or 'data + error'. How do you redesign the state?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Split the state into two parts: the content (the data you already have) and the loading/refresh status. Then you can show old data and a 'refreshing' or 'error' badge at the same time, instead of forcing the screen into a single Loading or Error box.

open as a page

Explain how 'singleton constants' for enums versus 'closed set of subtypes' for sealed types affects instance count, identity, and exhaustiveness guarantees the compiler can give.

level: seniorimportance: should knowfreq 35%

basics

~20 s

Enum entries are single shared instances, so you can compare them by identity and use them as map keys cheaply. Sealed subtypes can have unlimited instances. Both let the compiler verify a when covers every case, but it counts entries for enums and subtypes for sealed.

open as a page

For a public API value type with a small fixed set of cases, what trade-offs decide between an enum and a sealed type, especially around serialization and evolution?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Enums serialize simply by name and are easy to evolve by adding constants, but every case must share one shape. Sealed types let cases carry different data and add type-safety, but need explicit polymorphic serialization and are harder to extend across module boundaries.

open as a page

How do branch guards (`if`/`when` with conditions) interact with exhaustiveness over a sealed type, and when would you deliberately keep or avoid an `else`?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

If a branch only matches a subtype under an extra condition, the compiler can't prove every value is handled, so you must add another branch or an else. Use else only when you truly accept any other case; otherwise list subtypes so new ones break the build.

open as a page

What are the consequences of adding a new subtype to a public sealed type, and how do sealed types interact with generic type parameters and variance?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Adding a subtype to a public sealed type is a breaking change for code that handles every case, because that code now misses one. Sealed types can be generic, and you can make the type parameter covariant with out to allow flexible subtyping.

open as a page

Across a codebase, sealed state types accumulate scattered `when (state)` blocks. A teammate proposes adding a `fold`/visitor-style method on the sealed type instead. When is encapsulating behaviour on the ADT worth it, and what do you lose?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

A fold is a single method that takes one function per case and returns a result, so callers don't write their own when each time. It's handy for common reductions, but plain when is clearer for one-off logic and keeps the data and behaviour separate.

open as a page