skip to content

How do `expect` declarations in `commonMain` relate to `actual` implementations, and what are the rules and pitfalls?

level: seniorimportance: should knowfreq 40%

answer

  1. expect = bodyless contract in common; actual per target
  2. Compiler requires exactly one matching actual per target
  3. actual typealias maps to platform type
  4. Defaults only on expect side
  5. Prefer interface+DI for swappable collaborators

basics

~10 s

In commonMain you write expect declarations — promises of an API. Each target supplies a matching actual. The compiler checks every target provides one; common code calls the expected API without knowing the platform.

solid answer

~50 s

`expect` declarations in `commonMain` define a contract — a function, property, class, or `typealias` — that common code can use, while each target source set provides the `actual` implementation. The compiler enforces that **every** target has exactly one matching `actual` with the same signature, visibility, and name; a missing `actual` is a compile error for that target. `expect class` members and `expect fun`/`val` are common forms; `actual typealias` lets a target map an expected class onto an existing platform type (e.g. `actual typealias UUID = java.util.UUID` on JVM). Default values for parameters live only on the `expect` side. Pitfalls: signature drift between `expect` and `actual`, trying to give `expect` a body, and overusing `expect`/`actual` where a plain interface plus dependency injection would be cleaner and more testable. Intermediate source sets can also host `actual`s shared by several targets.

code

kotlin · 11 lines
kotlin
// commonMain
expect fun currentEpochMs(): Long

// jvmMain
actual fun currentEpochMs(): Long = System.currentTimeMillis()

// iosMain
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
actual fun currentEpochMs(): Long =
    (NSDate().timeIntervalSince1970 * 1000).toLong()

go deeper

for a junior

Knows expect is declared in common and actual provides the per-platform implementation.

for a middle

States the compiler's completeness and signature-match rules and writes a correct pair including actual typealias.

for a senior

Weighs expect/actual against interfaces + DI for testability, and knows defaults live only on expect and actuals can sit in intermediate source sets.

for a principal

Defines team guidance on when to use each mechanism, anticipates target-addition breakage, and shapes the common API surface for long-term maintainability.

## The mechanism `expect`/`actual` is Kotlin Multiplatform's primary way to let **common code call platform-specific implementations**. In `commonMain` you declare an `expect` member — it has a signature but **no body**. Each target source set (`jvmMain`, `iosMain`, …) must provide a matching `actual`. ```kotlin // commonMain expect fun randomUUID(): String expect val platformName: String expect class HttpEngine() { fun send(url: String): String } ``` ```kotlin // jvmMain actual fun randomUUID(): String = java.util.UUID.randomUUID().toString() actual val platformName: String = "JVM" actual class HttpEngine actual constructor() { actual fun send(url: String): String = /* use java.net.http */ "..." } ``` ## Compiler rules - **Completeness:** every declared target must supply exactly one `actual`. A missing one is a **compile error** for that target. - **Signature match:** `actual` must match the `expect` name, parameter types, return type, and visibility. Drift causes an error. - **No body on `expect`:** the `expect` side is a declaration only. (`expect class` members likewise have no body.) - **Default arguments** for parameters are declared **only on `expect`**, never repeated on `actual`. - **`actual typealias`:** a target can satisfy an `expect class` by aliasing an existing platform type: ```kotlin // commonMain expect class AtomicInt(initial: Int) { fun increment(): Int } // jvmMain actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger ``` ## Where `actual`s can live Not only leaf targets — an **intermediate source set** (e.g. a shared `nativeMain`) can host an `actual` reused by several native targets, reducing duplication. (The hierarchy mechanics are a sibling topic; here the point is the `actual` need not be in the leaf target.) ## `expect`/`actual` vs. interfaces `expect`/`actual` couples common code to a platform abstraction at compile time. Often a **plain interface declared in `commonMain` + a platform implementation injected at runtime** is cleaner: it's easier to fake in `commonTest`, avoids signature-drift friction, and keeps construction explicit. Reserve `expect`/`actual` for things that are genuinely a single global capability (UUID generation, current time, atomics) rather than swappable collaborators. ## Common pitfalls - Signature/visibility drift between `expect` and `actual`. - Putting a body on the `expect` side. - Repeating default values on `actual`. - Forgetting an `actual` for a newly added target → build break only when that target compiles. - Overusing `expect class` where an interface is more testable. ## Key takeaways - `expect` = contract in common; `actual` = per-target implementation, fully checked by the compiler. - `actual typealias` maps onto existing platform types. - Defaults only on `expect`; signatures must match exactly. - Prefer interfaces + DI when the dependency is a swappable collaborator.

  • What happens if you add a new target but forget its `actual`?
    Compilation for that target fails with a missing-`actual` error. Existing targets still compile, so the break surfaces only when the new target is built.
  • When would you choose an interface over `expect`/`actual`?
    When the dependency is a swappable collaborator you want to fake in `commonTest` or inject at runtime — interfaces are more testable and avoid `expect`/`actual` signature-drift friction.

saying these in an interview costs you the question

  • Giving the `expect` declaration a function body
  • Repeating default parameter values on the `actual` side
  • Thinking a missing `actual` is a runtime error
  • Believing `actual` can change the signature/visibility
  • Never considering interfaces + DI as an alternative

context