Contents

Contents

Kotlin 2.3.0: Nested Type Aliases, Return in Expression Bodies, JDK 25 Support, and More

Kotlin 2.3.0: Nested Type Aliases, Return in Expression Bodies, JDK 25 Support, and More

Since version 1.5, the Kotlin team has kept its promise to release a new major version almost every 6 months. While most of us were spending our time celebrating the holidays, the new Kotlin 2.3 just dropped last month, featuring a few interesting quality-of-life improvements. These include nested type aliases, return statements in expression bodies, data-flow-based exhaustiveness checks for when expressions, cool experimental features, and bug fixes. Let’s dive in!

Note on the IntelliJ IDEA Update
As always, before using features from the new Kotlin version, please update your IDE. Failing to do so may result in misleading errors or warnings about disabled experimental features and invalid syntax, even if you have updated the Kotlin version in your project build files.

Data-flow-based Exhaustiveness Checks for when Expressions

I believe it is the most underrated update in this release. Data-flow analysis (KT-76635, KEEP) has been a core part of Kotlin since 1.0, powering the language's famous smart casting and null safety. In Kotlin 2.3.0, this mechanism is further improved with data-flow-based exhaustiveness checks for when expressions.

Consider the example below:

sealed interface DrawResult {
   object Success : DrawResult
   object Skipped : DrawResult
   object Failure : DrawResult
}

fun DrawResult?.toBoolean(): Boolean {
   if (this == null) return false
   if (this == DrawResult.Failure) return false
   return when (this) {
       DrawResult.Success -> true
       DrawResult.Skipped -> false
       DrawResult.Failure -> false // Skipping this results is a compiler error in older versions
   }
}

The smart casting introduced by the if (this == null) statement works flawlessly – we don’t have to check the nullability of this in the when expression because the compiler knows it was already handled.

Unfortunately, before Kotlin 2.3.0, the compiler struggled with the second check (if (this == DrawResult.Failure)). We still had to specify the third option (or else) in the when block (DrawResult.Failure -> false) because exhaustiveness checking worked locally.

With Kotlin 2.3.0, this is resolved:

fun DrawResult?.toBoolean(): Boolean {
   if (this == null) return false
   if (this == DrawResult.Failure) return false
   return when (this) {
       DrawResult.Success -> true
       DrawResult.Skipped -> false
   }
}

The third check in the when expression is no longer needed. The compiler analyzes the data flow of the this value and determines that it can no longer be DrawResult.Failure; if it were, the function execution would have returned early in the check above.

Previously, the compiler primarily stored "positive" information for smart casting (i.e., knowing a value is a subtype because of a check). With this change, it now effectively tracks "negative" information as well – knowing what a value cannot be.

Nested Type Aliases

Kotlin 2.3.0 introduces nested type aliases (KT-45285, KEEP) as a stable feature (previously beta in 2.2.0). You can now define a typealias inside other declarations, including classes and interfaces. Previously, this was only possible as a top-level declaration. This allows you to improve readability in your internal implementations without cluttering the package-level namespace.

class ShakeFactory {
   typealias Mixer = (Banana) -> Shake
}

Remember that nested type aliases have limitations that are logical consequences of their nesting. For example, you cannot expose more visibility than the referenced type allows:

class ShakeFactory {
   private class Shaker
   typealias Mixer = (Shaker, Banana) -> Shake // error: 'public' typealias exposes 'private-in-class' in expanded type argument 'Shaker'
}

Additionally, you cannot use the generic type parameters of the outer class inside the nested type alias. This aligns with the existing behavior of non-inner nested classes.

You can read more about nested type aliases and other features introduced in Kotlin 2.2 here: Leveling up Kotlin: What’s New in 2.2

return Statement in Expression Bodies

Kotlin 2.3.0 now permits the use of return statements within expression bodies (KT-76926). This makes implementing fail-fast logic (guard clauses) in expressions much more effective.

Before Kotlin 2.3.0, the let scope function was a popular workaround for passing a value to a function that didn’t support nullable arguments, as seen here:

fun drawBananaIfNotNull(banana: Banana?): DrawResult =
   banana?.let { drawBanana(it) } ?: DrawResult.Skipped

Starting from Kotlin 2.3.0, you can achieve this in a cleaner way, skipping the let function entirely:

fun drawBananaIfNotNull(banana: Banana?): DrawResult =
   drawBanana(banana ?: return DrawResult.Skipped)

Please note that this works only for expression-body functions with an explicitly specified return type.

kotlin.time.Clock and kotlin.time.Instant

In Kotlin 2.1.20, the kotlin.time.Clock and kotlin.time.Instant (GitHub Issue, Docs) APIs were experimentally moved from kotlinx-datetime to the standard library. In Kotlin 2.3.0, this change is stable and accessible by default.

This move allows kotlinx-datetime to focus on calendar and timezone-aware operations, while keeping Clock, Instant, Duration, and TimeSource in the standard library, as they are essential for representing base computer time. The main purpose of this API is to provide a Java-inspired multiplatform date-time interface, which acts as a wrapper on the JVM with idiomatic Kotlin extensions.

JDK 25 support

Kotlin 2.3.0 adds support for JDK 25 LTS. As a reminder, support for the previous LTS (JVM 21) will continue until 2028-2031 for most distributions, so there is no rush to update your production applications yet.

However, you might consider upgrading to unlock performance wins, such as Compact Object Headers (JEP 519) for memory reduction, Synchronize Virtual Threads without Pinning (JEP 491) to solve compatibility issues with blocking code, and Scoped Values (JEP 506), which are finally standard. You also gain startup improvements via AOT Command-Line Ergonomics (JEP 514).

(Experimental) Explicit Backing Fields

Kotlin 2.3.0 introduces explicit backing fields (KEEP, KT-14663) as an experimental feature. This highly anticipated improvement allows you to define a property’s backing field directly, whereas previous versions required defining two separate variables (e.g., banana and _banana):

class BananaBlender {
   private val _bananas = mutableListOf<Banana>()
   val bananas: List<Banana> = _bananas

   fun add(newBanana: Banana) {
       _bananas.add(newBanana)
   }
}

With the new syntax, you can do this instead:

class BananaBlender {
   val bananas: List<Banana>
       field = mutableListOf<Banana>()
   fun add(newBanana: Banana) {
       bananas.add(newBanana)
   }
}
fun blend() {
   val blender = BananaBlender()
   // blender.bananas.add(Banana()) <-- error: Unresolved reference 'add'
   blender.add(Banana())
}

This feature introduces the field keyword to define the backing field. It allows the bananas property to be smart-casted inside the class scope to the type of its backing field (MutableList in this case). The field is always private, so it can only be accessed within the BananaBlender class.

You can also skip the initialization for the backing field (though you must then initialize it in the init block, similar to a standard property):

val bananas: List<Banana>
   field: MutableList<Banana>
init {
   bananas = emptyList()
}

While placing the field keyword on the next line helps with readability, you are free to format it on a single line as well:

val bananas: List<Banana> field = mutableListOf<Banana>()

Summary

The features in 2.3.0 make the compiler smarter and your code cleaner. It’s great to see the momentum in language improvements following the K2 release. I’m keeping my fingers crossed for the future, hoping that the next major version includes the features I've been waiting for. Curious about what I think is still missing from Kotlin? Check out this post: Kotlin: What’s Missing and How to Work Around It

Reviewed by: Rafał Maciak

Blog Comments powered by Disqus.