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
- 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. - For each variable in the parentheses, from left to right, the compiler emits a call to the next
componentN(): the first variable getscomponent1(), the second getscomponent2(), and so on. - Each
componentN()call happens exactly once per destructured variable. If acomponent1()implementation has side effects (logging, mutating state), those effects run once, at the point of destructuring, not lazily. - If you use
_for a position, the correspondingcomponentN()is still called (its side effects still happen) — only the resulting value is discarded rather than bound. - If any required
componentN()function is absent, unresolved, or not markedoperator, 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, andMap.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 implicitit.
Practice Exercises
- Write a function that takes a
List<Pair<String, Int>>of names and ages, and uses destructuring in aforloop to print a line likeName is Age years oldfor each entry. - Define
data class Rectangle(val width: Int, val height: Int). Write a function that destructures aRectangleinternally and returns its area and perimeter as aPair<Int, Int>, then destructure that returned pair where you call the function. For a 4 by 5 rectangle, the expected output isarea=20, perimeter=18. - Create a plain (non-data) class
Fraction(val numerator: Int, val denominator: Int), addoperator fun component1()andoperator fun component2()to it, then destructure an instance and print it in the formnumerator/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 aListalready 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 orvarwhen needed, but you cannot mixvalandvarwithin one declaration. - Prefer destructuring for small, obvious tuples and named property access everywhere else, especially in code that outlives the current refactor.
