Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

News Tutorials

The Road to Name-Based Destructuring

TL;DR

  • New “val inside parentheses” syntax is being introduced to allow for name-based destructuring. Additionally, new syntax with square brackets is being introduced for positional destructuring.
    • Both are currently Experimental (enabled using the -Xname-based-destructuring=only-syntax compiler argument) and will become Stable in a future release.
  • In the distant future, the behavior of the “val outside parentheses” syntax for destructuring will change from being position-based to name-based.
    • There will be a long migration period before the default changes, and tooling is already in place to help with migration.
    • You can already make the switch to the new behavior (-Xname-based-destructuring=complete), but note its Experimental status.
  • The compiler ships with migration helpers that will be enabled by default for a few versions, and it will be some time before the new behavior becomes the default.
    • You can enable these helpers now by using -Xname-based-destructuring=name-mismatch.

Kotlin is changing, with names set to become central in destructuring. In the future, val (name, age) = person will extract the name and age properties from the person value, regardless of the way and order in which they were defined. This marks a change from the current approach to destructuring, in which the position is the key element. This blog post explains the reasoning behind this change, the migration strategy, and how Kotlin’s tooling supports it.

Why destructure by name instead of position?

Destructuring is most commonly used to access properties from data classes. For example, we can define a Person class as follows:

data class Person(val name: String, val age: Int)

Then we can extract several of the primary properties in a single go. This is what we call destructuring the value into its components.

fun isValidPerson(p: Person) {
  val (name, age) = p
  return name.isNotEmpty() && age >= 0
}

Currently, destructuring is done by position. The variables we introduce in a destructuring declaration often follow the names of the properties in the data class, but there’s no such requirement in the language.

// this is exactly the same function as above
fun isValidPerson(p: Person) {
  val (foo, bar) = p
  return foo.isNotEmpty() && bar >= 0
}

This lack of connection can cause problems, as it is very easy to inadvertently swap the order of two properties. This mistake may be caught later because of non-matching types, but it appears far from the actual origin.

The way in which components relate to primary properties also hinders refactoring. For example, we cannot move the age property to be computed and still retain the nice data class syntax. Imagine we make the following change:

data class Person(val name: String, val birthdate: Date) {
  val age = (Date.now() - birthdate).years
}

Now every destructuring declaration suddenly changes from age to birthdate! To be clear, source compatibility is still possible, but you need to do a lot more work.

The current approach to destructuring is also at odds with abstraction. If we turned Person into an interface, previous instances of destructuring would no longer be valid. We could work around this by introducing our own component functions, but this is usually seen as advanced. As a result, most interfaces do not provide such facilities.

interface Person {
  val name: String
  val age: Int

  operator fun component1(): String = name
  operator fun component2(): Int    = age
}

These problems go away if destructuring depends on names instead of positions. It doesn’t matter if you rearrange the order, change a computed property into a primary one or vice versa, or define a property in a class, interface, or object. The property’s name is a stable characteristic, which means that the source does not require any changes.

The new syntax

You can enable the new syntax by passing -Xname-based-destructuring=only-syntax as a compiler argument.

Without further ado, let’s look at the new syntax, which uses names for destructuring. Instead of a single val outside of the parentheses, you use val for each property inside the parentheses.

fun isValidPerson(p: Person): Boolean {
  (val name, val age) = p
  return name.isNotEmpty() && age >= 0
}

As expected, the order in which we write val name and val age in the example above doesn’t matter. This new syntax also supports renaming for cases in which the new variable you want to define is not the same as the property you want to access.

fun isValidPerson(p: Person): Boolean {
  (val age, val theName = name) = p
  return theName.isNotEmpty() && age >= 0
}

Destructuring based on position is still important for a few use cases. Pairs and triples, for example, don’t have names for their components at a conceptual level, and there’s no intention to require littering code that uses them with first and second. Position-based destructuring can also be used for collections, and in that case, there are no available properties. The new syntax for position-based destructuring uses square brackets – mirroring the syntax of upcoming collection literals. You can choose whether to put val inside or outside the brackets.

fun isZero(point: Pair<Int, Int>): Boolean {
  val [x, y] = point      // one way
  [val x, val y] = point  // or another
  return x == 0 && y == 0
}

All of this new syntax is available anywhere you can destructure, including lambda expressions and loops.

// suggested new syntax for iterating through a map
for ([key, value] in map) {
  // work with each entry
}

person?.let { (val name, val years = age) -> "$name is $years years old" }

To reiterate: This is all new syntax. As of version 2.3.20, the compiler knows what it means, and we intend to keep this syntax once the feature reaches Stable status.

Repurposing parentheses

At some point in the future, we intend for all destructuring using parentheses to be name-based. You can actually experience this future now by using the -Xname-based-destructuring=complete compiler argument.

If you already have a project, though, making the switch could have a major impact. The most visible issue would be if destructuring stops working, and the code needs to be updated. A more dangerous one would be destructuring declarations that remain valid but change the meaning.

For that reason, the compiler ships a migration helper under the -Xname-based-destructuring=name-mismatch compiler argument. When enabled, the compiler gives a warning in cases where the behavior is inconsistent between position-based and name-based destructuring or where the code won’t be accepted once destructuring with parentheses is no longer positional.

// accepted by both with the same behavior 
val (name, age) = person

// warning: accepted by both, but the behavior changes
val (age, name) = person

// warning: accepted only by position-based destructuring
val (personName, personAge) = person
// the IDE suggests potential fixes
// - renaming: (val personName = name, val personAge = age) = person
// - square brackets: val [personName, personAge] = person

The future

As hinted in this post, there will be ample time to migrate to the new name-based destructuring. Our current timeline looks as follows:

  • As of version 2.3.20, name-based destructuring is Experimental, meaning that you need a special compiler argument to use it.
    • Support in IntelliJ IDEA may be lacking, especially for migration.
  • With version 2.5.0 (at the end of 2026), the feature will become Stable.
    • The new syntax will be available without additional configuration.
    • The compiler will start reporting migration hints, and IntelliJ IDEA will include inspections and quick-fixes to help with the process.
    • This stage roughly corresponds to name-mismatch in compiler arguments, although we may make some adjustments to reporting depending on user feedback.
  • By version 2.7.0 (at the end of 2027), destructuring with parentheses will be name-based.
    • You can migrate to this stage earlier by using complete in compiler arguments.

This is a big change, and we don’t want to rush it. If at any point during 2027 it becomes clear that the ecosystem is not ready, we may postpone the change until another major version.

At no point are we deprecating the generation of component functions for data classes. Data classes will still generate the same bytecode – name-based destructuring is a feature for use sites. However, we plan to introduce multi-field value classes without component functions. That means that destructuring for value classes will only be name-based.

References