1. Why you need spread *
When you write vararg, a function call looks nice: sumAll(1, 2, 3). But real life loves to throw situations at you where the numbers (or strings) are already in an array. For example, you collected a “tail” of parameters, read them from input, or saved results from several game rounds. And that’s where the classic beginner question appears: “why can’t I just pass an array?”
Let’s put into words what’s going on. vararg is a form of call where you pass 0..N separate arguments. An array, on the other hand, is a single object (a single variable) that contains N elements. And the compiler honestly treats the array as one argument, not as a “container that should be unpacked.”
A visual diagram (simplified):
flowchart LR
A["vararg expects: a, b, c (multiple arguments)"] --> B["call f(1, 2, 3)"]
C["you have: arr = [1, 2, 3] (one variable)"] --> D["call f(arr) — that is one argument"]
E["spread: *arr"] --> F["call f(*arr) — that is f(1, 2, 3)"]
In Kotlin, the spread operator * exists to solve this problem: it tells the compiler “take the array elements and pass them as separate arguments.”
2. Spread for Array<T>
Let’s start with the friendliest case: a regular Array<String> or Array<Int>, i.e., an “object” array. It’s created via arrayOf(...). With this kind of array, spread reads as naturally as possible: f(*arr).
Minimal example: printing lines
fun printLines(vararg lines: String) {
for (line in lines) {
println(line)
}
}
fun main() {
val tail: Array<String> = arrayOf("two", "three")
printLines("one", *tail)
// one
// two
// three
}
Notice an important detail: *tail can be used only in the argument position when calling a function. This is not an “operation on the array,” but call-site syntax.
Bad call (and why it’s bad)
If you try to do this (I’ll leave it as a comment so your project still compiles):
fun main() {
val tail = arrayOf("two", "three")
printLines("one", tail)
// Compilation error: tail is a single argument of type Array<String>,
// but the function expects String, String, String... (vararg)
}
The compiler won’t “guess” that you meant to “insert the array elements into the argument list.” To do that, you must say it explicitly: *tail.
Mixed call: some arguments manually, some from an array
After you see f(*arr), the next practical scenario comes up almost immediately: “can I specify a couple of values by hand and take the rest from the array?” Yes: you can pass normal arguments before and after the unpacking.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
val mid: Array<Int> = arrayOf(2, 3, 4)
println(sumAll(1, *mid, 10)) // 20
}
The key idea here: *mid doesn’t replace the whole argument list; it’s “spliced” into it.
If it feels like “inserting a stack of pages into the middle of a notebook”—yes, that analogy is pretty fair. Only the notebook doesn’t complain about types, while Kotlin does, and that’s actually useful.
3. Primitive arrays and conversions
Now we move to the point where beginners start suspecting Kotlin is alive. The thing is, Kotlin has two families of arrays: Array<T> (for objects) and separate types for primitives: IntArray, DoubleArray, CharArray, etc. They exist so numbers can be stored efficiently, without “boxing into objects.”
Practical consequence: not every array can be “unpacked” into every vararg. The array type has to match what the vararg parameter expects.
vararg Int and IntArray: a good scenario
If your function is declared as fun sumAll(vararg xs: Int), then inside, xs is essentially an IntArray. And you can pass an IntArray into that vararg via spread:
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
val attempts: IntArray = intArrayOf(3, 5, 2)
println(sumAll(*attempts)) // 10
}
This feels “logical”: IntArray contains Int, and vararg xs: Int accepts Int.
IntArray is not Array<Int> (and sometimes that matters)
But Array<Int> is a different type. It’s created via arrayOf(1, 2, 3). And you can’t just put an equals sign between them, because these really are different structures.
Practical takeaway: sometimes you’ll have to convert to “move” from one array family to the other.
When you need toTypedArray()
Most often “it doesn’t compile” happens in this situation: you have an IntArray, but the function accepts vararg not Int, but, for example, Any? (people often do this for a “universal logger”) or some generic type. And at that point, IntArray no longer fits, because it is not an Array<...>.
Example: the logger takes Any?, but we have an IntArray
fun log(tag: String, vararg items: Any?) {
print("[$tag] ")
for (item in items) {
print("$item ")
}
println()
}
fun main() {
val attempts: IntArray = intArrayOf(3, 5, 2)
val boxed: Array<Int> = attempts.toTypedArray()
log("GAME", *boxed)
// [GAME] 3 5 2
}
There are two steps here, and both matter:
1) attempts.toTypedArray() turns an IntArray into an Array<Int> (i.e., an “object” array).
2) *boxed unpacks Array<Int> into the vararg items.
Why can’t you just write log("GAME", *attempts)?
Because attempts is an IntArray, while vararg items: Any? inside the function is an Array<out Any?>. Those types have different nature. Kotlin can’t automatically “pretend” that IntArray is an object array: the storage format is different.
In plain terms: IntArray is a “box of digits,” while Array<Int> is a “box of tiny number-objects.” Spread can dump out a box of the right type, but the box has to match in shape.
Cheat sheet: what unpacks into what
| What you have in a variable | What the vararg expects | What to write at the call site |
|---|---|---|
|
|
|
|
|
|
|
|
|
This table doesn’t cover “every possible situation,” but for today’s level, it’s 90% of practical cases.
4. Forwarding vararg to another vararg function
A common scenario comes up when you build utilities. You wrote one convenient function with vararg, and then you want to write another one that adds a “header,” “prefix,” “signature”—and calls the first one inside. And it’s easy to forget that inside the second function, vararg has already turned into an array, so to pass it along you need *.
This follows directly from the definition: inside a function, a vararg parameter is used as an array.
Example: printing with a header
fun printLines(vararg lines: String) {
for (line in lines) println(line)
}
fun printWithHeader(header: String, vararg lines: String) {
println(header)
printLines(*lines) // important: *lines
}
fun main() {
printWithHeader("== MENU ==", "Play", "Settings", "Exit")
// == MENU ==
// Play
// Settings
// Exit
}
If you remove *, you’ll be attempting to pass an Array<String> where String, String, String... (vararg) are expected. That’s the same typical conflict, just “hidden” inside your own utility.
5. Example: stats for the “Guess the Number” game
So it doesn’t feel like spread exists only for abstract textbook examples, let’s embed it into a clear piece of a console app. We’ve already made the “Guess the Number” game: it has rounds, attempts, final messages. A logical next step is to learn how to nicely print statistics across multiple rounds without copying elements by hand.
Let’s say we played several rounds and saved the number of attempts in an IntArray. Now we want to print them via a function that takes vararg Int.
Utility: print stats
fun printAttempts(vararg attempts: Int) {
print("Attempts: ")
for (a in attempts) print("$a ")
println()
}
fun main() {
val attemptsByRound: IntArray = intArrayOf(4, 2, 6)
printAttempts(*attemptsByRound) // unpacked IntArray
// Attempts: 4 2 6
}
Important point: you didn’t change printAttempts to make it “know about arrays.” It stays convenient for calls like printAttempts(3, 5, 1), but you can still “feed” it an array via *.
The same output, but with a universal logger
If you want “one logging function for everything,” it often accepts Any?, so you can pass strings, numbers, and null.
fun log(tag: String, vararg items: Any?) {
print("[$tag] ")
for (item in items) print("$item ")
println()
}
fun main() {
val attemptsByRound: IntArray = intArrayOf(4, 2, 6)
log("GAME", *attemptsByRound.toTypedArray())
// [GAME] 4 2 6
}
The combo toTypedArray() + * is like “repack your stuff into the right suitcase format, then unzip it.” It looks a bit longer, but it’s transparent in meaning.
6. Common mistakes when working with spread * and vararg
Mistake #1: passing an array into vararg without * and wondering “why Kotlin is being stubborn.”
Usually it looks like this: printLines(linesArray) instead of printLines(*linesArray). The compiler is right: you passed one argument of type Array<String>, while the function expects many arguments of type String. The habit “if you need vararg, remember *” saves a lot of time.
Mistake #2: trying to use * outside a function call.
The spread operator is not an “operation on the array,” not a “special loop,” and not “unpacking magic everywhere.” It works in a specific position: before the array name in a function’s argument list. If you try to apply * in another context, you’ll get a syntax error—and that’s normal.
Mistake #3: mixing up Array<Int> and IntArray and getting stuck in conversions.
In practice, it shows up like this: you have an IntArray, but you try to pass it where an Array<...> is expected (for example, vararg Any?), or vice versa. It’s useful to remember that primitive arrays are a separate “type line” and do not inherit from Array, even though they look similar. If types don’t match, you either choose the correct array kind when creating it, or perform an explicit conversion.
Mistake #4: forgetting * when forwarding vararg into another vararg function.
A very common bug in utilities: you write fun a(vararg xs: String), then fun b(vararg xs: String) { a(xs) }—and you get a type error. The reason is simple: inside b, the parameter xs is already an array, and when passing it “as a set of arguments,” you need to unpack it: a(*xs). This is a direct consequence of the fact that vararg is used as an array inside the function.
Mistake #5: using toTypedArray() “on autopilot” and not understanding why.
toTypedArray() is a useful tool, but it’s not always needed. If your function accepts vararg Int and you already have an IntArray, most often f(*ints) is enough. Conversion becomes necessary when you cross between a “primitive” array and an “object” array, or when the vararg takes a type like Any?.