How to Define Custom Conditions for a Kotlin When Expression: 3 Advanced Techniques
You can define custom conditions in Kotlin when expressions by using omitted-subject syntax for arbitrary Boolean logic, implementing the contains operator for custom in checks, or leveraging sealed class hierarchies for exhaustive type matching.
Kotlin’s when construct offers far more flexibility than traditional switch statements, allowing you to tailor branch conditions to complex domain logic. Whether you need to check membership in custom ranges, validate against business rules, or handle sealed type hierarchies, the compiler supports these patterns through specific PSI and FIR representations.
Three Ways to Create Custom Conditions in Kotlin When Expressions
Omitted-Subject When Expressions for Arbitrary Boolean Logic
The most straightforward way to implement custom conditions is to omit the when subject entirely. In this form, each branch evaluates an independent Boolean expression, allowing you to combine multiple variables and complex predicates.
val x = 7
val y = 12
val result = when {
x < 0 && y < 0 -> "both negative"
x > 0 && y > 0 -> "both positive"
x % 2 == 0 -> "x is even"
else -> "fallback"
}
This pattern is parsed into the Kotlin compiler’s PSI structure in KtWhenExpression located at compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtWhenExpression.java, where the absence of a subject expression triggers Boolean evaluation mode for each entry.
Custom Infix Conditions Using the In Operator
You can create domain-specific membership tests by implementing the contains operator in your own classes. When you use in or !in in a when expression with a subject, Kotlin invokes operator fun contains(element: T): Boolean on the right-hand side.
data class TemperatureRange(val low: Int, val high: Int) {
operator fun contains(value: Int): Boolean = value in low..high
}
val cold = TemperatureRange(-30, 0)
val warm = TemperatureRange(1, 25)
val temp = 18
val description = when (temp) {
in cold -> "Freezing"
in warm -> "Comfortable"
else -> "Hot"
}
This technique allows you to encapsulate any validation logic—regex matching, database lookups, or geometric containment—into reusable condition objects. The FIR (Front-end Intermediate Representation) handling for these expressions is implemented in FirWhenExpressionImpl at compiler/fir/tree/gen/org/jetbrains/kotlin/fir/expressions/impl/FirWhenExpressionImpl.kt.
Type-Based Conditions with Sealed Class Hierarchies
For exhaustive custom conditions based on types, define a sealed class hierarchy. The compiler guarantees that your when expression handles every possible subtype, effectively turning type checks into compile-time verified conditions.
sealed class UiState
data class Loading(val percent: Int) : UiState()
object Success : UiState()
object Error : UiState()
fun render(state: UiState) = when (state) {
is Loading -> "Loading ${state.percent}%"
Success -> "All good!"
Error -> "Something went wrong"
}
Because UiState is sealed, the compiler knows all possible implementations at compile time. If you add a new subclass, the compiler will flag incomplete when expressions. This safety mechanism is constructed during FIR building in FirWhenExpressionBuilder at compiler/fir/tree/gen/org/jetbrains/kotlin/fir/expressions/builder/FirWhenExpressionBuilder.kt.
Implementation Details in the Kotlin Compiler
The Kotlin compiler processes when expressions through three main components in the JetBrains/kotlin repository:
-
KtWhenExpression(compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtWhenExpression.java) – Represents the PSI (Program Structure Interface) node for parsingwhensyntax, handling both subject-present and subject-omitted forms. -
FirWhenExpressionImpl(compiler/fir/tree/gen/org/jetbrains/kotlin/fir/expressions/impl/FirWhenExpressionImpl.kt) – The FIR implementation that modelswhenexpressions during semantic analysis, including support for custominandisconditions. -
FirWhenExpressionBuilder(compiler/fir/tree/gen/org/jetbrains/kotlin/fir/expressions/builder/FirWhenExpressionBuilder.kt) – Constructs FIR nodes forwhenexpressions, ensuring exhaustive checking for sealed classes and proper resolution of custom condition operators.
Practical Code Examples
Here are complete, runnable examples demonstrating each custom condition technique:
// Example 1: Omitted subject with complex logic
fun classify(x: Int, y: Int) = when {
x == y -> "equal"
x > y -> "x is larger"
else -> "y is larger"
}
// Example 2: Custom membership test
class PrimeRange {
operator fun contains(value: Int): Boolean =
value > 1 && (2..value/2).none { value % it == 0 }
}
val prime = PrimeRange()
val n = 13
val msg = when (n) {
in prime -> "$n is prime"
else -> "$n is composite"
}
// Example 3: Exhaustive sealed class handling
sealed class Result
data class Ok(val data: String) : Result()
object NotFound : Result()
object Unauthorized : Result()
fun handle(r: Result) = when (r) {
is Ok -> "Got ${r.data}"
NotFound -> "Missing"
Unauthorized -> "Denied"
}
Summary
- Omitted-subject syntax allows arbitrary Boolean expressions as custom conditions, parsed via
KtWhenExpressionin the compiler’s PSI layer. - Custom
inconditions leverage thecontainsoperator to encapsulate domain-specific validation logic, resolved during FIR generation inFirWhenExpressionImpl. - Sealed class hierarchies enable exhaustive, type-safe custom conditions with compile-time verification, constructed through
FirWhenExpressionBuilder.
Frequently Asked Questions
Can I use multiple conditions in a single when branch?
Yes, you can combine multiple conditions using || (or) logic by placing them on the same line with commas, or by using separate when entries. However, Kotlin does not support && (and) within a single branch condition directly; for complex conjunctions, use the omitted-subject form where each branch is a full Boolean expression.
How does the in operator work with custom conditions?
The in operator invokes the contains method on the right-hand operand. When you write value in container, the compiler translates this to container.contains(value). By defining operator fun contains(element: T): Boolean in your own classes, you create custom membership tests that integrate seamlessly with when expressions, as implemented in the FIR layer via FirWhenExpressionImpl.
Are sealed classes required for exhaustive when expressions?
Sealed classes are not strictly required, but they are the only mechanism that provides compile-time exhaustiveness checking for type-based conditions. If you use open classes or interfaces, the compiler will require an else branch because it cannot determine all possible subtypes. Sealed classes restrict the hierarchy to a known set of types, allowing the compiler—through FirWhenExpressionBuilder—to verify that all cases are handled without a default branch.
What is the performance impact of custom when conditions?
Custom conditions using the omitted-subject form or custom contains operators have negligible performance overhead compared to standard if-else chains, as they compile down to similar bytecode. The in operator with custom containers is inlined when possible. Sealed class type checks use the instanceof operator at the JVM level, which is highly optimized. According to the Kotlin compiler implementation in FirWhenExpressionImpl, these constructs are resolved during compilation rather than runtime, ensuring efficient execution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →