Destructuring Declarations

A destructuring declaration lets you unpack an object into several variables in one statement, instead of pulling each property out individually. Kotlin does not hardcode this for a fixed list of types — it works for anything that exposes a small family of operator functions named component1(), component2(), and so on. Data classes generate these functions automatically, which is why destructuring feels so natural with them, and Pair, Triple, map entries, lists, and even your own classes can opt in the same way. Used well, destructuring makes code that works with small bundles of related data — coordinates, key/value pairs, results — read far more clearly than a wall of .property access.

Overview / How it works

Write val (a, b) = someObject and the compiler rewrites it, roughly, into:

val a = someObject.component1()
val b = someObject.component2()

Nothing magic is happening — destructuring is pure syntactic sugar over ordinary operator function calls. For it to compile, the right-hand side’s type must have a component1() function, a component2() function, and so on for however many variables you list, each marked with the operator keyword. If a required componentN() is missing, the compiler rejects the declaration outright — there is no silent fallback.

Data classes get these functions for free: every property declared in the primary constructor becomes a componentN() in declaration order (properties declared in the class body do not count). Pair<A, B> and Triple<A, B, C> ship with component1()/component2()/component3() built into the standard library. Map.Entry has component1() (the key) and component2() (the value), which is what makes for ((key, value) in map) possible. List even has extension functions component1() through component5(), so you can destructure the first five elements of a list directly.

Because destructuring is positional rather than named, the variable names you choose on the left-hand side carry no meaning to the compiler — only their position does. component1() always fills the first variable, regardless of what you call it. This is the single most important thing to internalize about destructuring, and it is the source of the most common real-world bug, covered below.

You can also declare explicit types (val (x: Int, y: Int) = point), use var instead of val when you need to reassign the unpacked variables, and skip a position you don’t need with an underscore: val (_, age, city) = person. Underscore skipping still evaluates component1() for its side effects (if any) — it just discards the result instead of binding it to a name.

Syntax

val (name1, name2, ...) = expression
Part Meaning
val / var Whether the unpacked variables are read-only or reassignable. Applies to the whole group; you cannot mix val and var within one destructuring declaration.
(name1, name2, ...) The variables to bind, in order. Each one corresponds to component1(), component2(), etc. on the right-hand side’s type.
_ Used in place of a name to skip a position without binding it.
expression Any value whose type declares (or inherits, via extension functions) the required operator fun componentN() functions.

Examples

Example 1: Destructuring a Pair

fun main() {
    val pair = Pair("Alice", 29)
    val (name, age) = pair
    println("$name is $age years old")
}
Alice is 29 years old

Pair<String, Int> already provides component1() (returns first) and component2() (returns second), so name binds to "Alice" and age binds to 29 with no extra code required.

Example 2: Destructuring a data class

data class Point(val x: Int, val y: Int)

fun main() {
    val p = Point(10, 20)
    val (x, y) = p
    println("x=$x, y=$y")
    val doubled = Point(x * 2, y * 2)
    println(doubled)
}
x=10, y=20
Point(x=20, y=40)

data class Point auto-generates component1() returning x and component2() returning y, alongside the equals(), hashCode(), toString(), and copy() you get from any data class. The second println shows the generated toString() at work.

Example 3: Destructuring map entries

fun main() {
    val scores = mapOf("Alice" to 90, "Bob" to 85, "Carol" to 92)
    for ((name, score) in scores) {
        println("$name scored $score")
    }
}
Alice scored 90
Bob scored 85
Carol scored 92

Iterating a Map yields Map.Entry objects one at a time. Because Map.Entry declares component1() (the key) and component2() (the value), the for ((name, score) in scores) loop unpacks each entry directly instead of forcing you to write entry.key and entry.value. The same pattern works in lambdas, for example pairs.forEach { (number, word) -> println("$number -> $word") } when iterating a list of Pair values.

Example 4: Destructuring your own class

class UploadResult(val success: Boolean, val message: String) {
    operator fun component1() = success
    operator fun component2() = message
}

fun main() {
    val (ok, msg) = UploadResult(true, "Upload complete")
    println("ok=$ok, msg=$msg")
}
ok=true, msg=Upload complete

UploadResult is a plain class, not a data class, so nothing is generated automatically. Adding operator fun component1() and operator fun component2() by hand is enough to make destructuring work — the compiler doesn’t care how a type is defined, only that the right operator functions exist on it.

How it works step by step

  1. The compiler evaluates the right-hand side expression once into a hidden temporary value — so an expression with side effects on the right of = only runs once, no matter how many variables you destructure into.
  2. For each variable in the parentheses, from left to right, the compiler emits a call to the next componentN(): the first variable gets component1(), the second gets component2(), and so on.
  3. Each componentN() call happens exactly once per destructured variable. If a component1() implementation has side effects (logging, mutating state), those effects run once, at the point of destructuring, not lazily.
  4. If you use _ for a position, the corresponding componentN() is still called (its side effects still happen) — only the resulting value is discarded rather than bound.
  5. If any required componentN() function is absent, unresolved, or not marked operator, the whole declaration fails to compile with a message naming the missing function.

Common Mistakes

Mistake 1: Assuming destructuring is name-based, not position-based

Destructuring binds strictly by position. If a data class’s constructor parameters aren’t in the order you expect, the variable names on the left mean nothing to the compiler — this compiles cleanly but produces silently wrong values:

data class Point3D(val z: Int, val x: Int, val y: Int)

fun main() {
    val point = Point3D(z = 5, x = 1, y = 2)
    val (x, y, z) = point
    println("x=$x, y=$y, z=$z")
}
x=5, y=1, z=2

Because the constructor order is (z, x, y), component1() returns the z property, component2() returns x, and component3() returns y. The destructured variables end up holding completely different data than their names suggest, and nothing warns you. The safe fix is to access named properties instead of destructuring whenever the source type’s field order isn’t something you fully control or trust:

data class Point3D(val z: Int, val x: Int, val y: Int)

fun main() {
    val point = Point3D(z = 5, x = 1, y = 2)
    println("x=${point.x}, y=${point.y}, z=${point.z}")
}
x=1, y=2, z=5

Mistake 2: Destructuring a type with no component functions

Destructuring only works when the type actually provides the operator functions. A plain class that isn’t a data class, and hasn’t defined them manually, fails to compile:

class Point(val x: Int, val y: Int)

fun main() {
    val point = Point(3, 4)
    val (x, y) = point
    println("$x, $y")
}
// error: destructuring declaration initializer of type Point must have a 'component1()' function

The fix is either to add the operator functions by hand or, for a simple value holder like this, to make it a data class so the compiler generates them:

data class Point(val x: Int, val y: Int)

fun main() {
    val point = Point(3, 4)
    val (x, y) = point
    println("$x, $y")
}
3, 4

Best Practices

  • Reach for destructuring with data classes, Pair, Triple, and Map.Entry, where the meaning of each position is obvious from context (loop over a map, unpack a coordinate).
  • Avoid destructuring more than two or three components; beyond that, named property access reads more clearly and doesn’t hide which value is which.
  • Remember destructuring is positional. Reordering a data class’s primary constructor parameters silently changes the meaning at every destructuring call site without a compile error, as long as the types line up. Prefer named property access (point.x) in code that must survive future refactors, and reserve destructuring for small, local, easy-to-verify scopes.
  • Use _ to skip a component you don’t need instead of naming it and never using it.
  • Only add custom component1()/component2() operator functions when the positions have an obvious, stable order (a result’s success flag and message, a range’s start and end) — not for classes with many equally important properties.
  • In lambdas, destructure only when the element type is a Pair, Map.Entry, or similar tuple-like value; for a single value, just use the implicit it.

Practice Exercises

  • Write a function that takes a List<Pair<String, Int>> of names and ages, and uses destructuring in a for loop to print a line like Name is Age years old for each entry.
  • Define data class Rectangle(val width: Int, val height: Int). Write a function that destructures a Rectangle internally and returns its area and perimeter as a Pair<Int, Int>, then destructure that returned pair where you call the function. For a 4 by 5 rectangle, the expected output is area=20, perimeter=18.
  • Create a plain (non-data) class Fraction(val numerator: Int, val denominator: Int), add operator fun component1() and operator fun component2() to it, then destructure an instance and print it in the form numerator/denominator.

Summary

  • Destructuring declarations unpack a value into several variables using operator fun component1(), component2(), and so on.
  • Data classes generate componentN() for every primary-constructor property automatically; plain classes need it added by hand.
  • Pair, Triple, Map.Entry, and the first five elements of a List already support destructuring out of the box.
  • Destructuring is strictly positional, not name-based — the compiler binds by order, so reordering a data class’s constructor parameters silently changes what every destructuring call site means.
  • Use _ to skip a position; use explicit types or var when needed, but you cannot mix val and var within one declaration.
  • Prefer destructuring for small, obvious tuples and named property access everywhere else, especially in code that outlives the current refactor.