skip to content

How does expect/actual work in a KMP module, and where do you place each declaration across source sets?

level: middleimportance: must knowfreq 60%

answer

  1. expect in commonMain (no body)
  2. actual in each platform set (body)
  3. missing actual = compile error
  4. actual typealias reuses platform type
  5. place actual in most-specific covering source set

basics

~20 s

You write an expect declaration in commonMain describing an API with no body, then provide a matching actual implementation in each platform source set. The compiler links them so common code can call the platform-specific implementation.

solid answer

~50 s

`expect` declares a function, property, class, or object in `commonMain` with the signature but **no implementation**. Each target source set that participates must provide a matching `actual` declaration that supplies the platform-specific body. The compiler checks that every `expect` has an `actual` for every target it compiles to — a missing `actual` is a compile error. Matching is by signature/name. Modern Kotlin also supports **`actual typealias`** (mapping an `expect class` onto an existing platform type, e.g. `actual typealias UUID = java.util.UUID` on JVM) and allows defaults: an `expect fun` can have a default in common that some targets override. Place `expect` in `commonMain`; place each `actual` in the most-specific source set that covers the needed targets — often a single platform set, but an intermediate set (e.g. `appleMain`) can hold one `actual` shared by several Apple targets.

code

kotlin · 12 lines
kotlin
// commonMain
expect class Platform() {
    val name: String
}

// jvmMain
actual class Platform actual constructor() {
    actual val name: String = "JVM"
}

// jvmMain alternative via typealias
// actual typealias UUID = java.util.UUID

go deeper

for a junior

Knows expect goes in common with no body and actual provides the platform implementation.

for a middle

Explains compiler enforcement across all targets, actual typealias, and placing actuals in intermediate sets.

for a senior

Chooses expect/actual vs. interface-injection deliberately and minimizes the expect surface for maintainability.

for a principal

Sets module-wide conventions for when platform abstraction uses expect/actual vs. DI, weighing binary-compatibility and API evolution.

## The expect/actual mechanism KMP lets you call platform-specific code from shared code through the **`expect`/`actual`** keyword pair: - **`expect`** — declared in `commonMain`. It states *what* exists (signature, name, type) but has **no body**. It's a contract the common code can compile against. - **`actual`** — declared in a platform source set. It provides *how* the contract is implemented for that platform. ```kotlin // commonMain expect fun platformName(): String // jvmMain actual fun platformName(): String = "JVM ${System.getProperty("java.version")}" // iosArm64Main (or a shared appleMain) actual fun platformName(): String = "iOS" ``` ## What can be expect/actual Functions, top-level **properties**, **classes**, **interfaces**, **objects**, and **annotations**. For a class, every member declared in the `expect class` must be matched by the `actual class`. ## actual typealias Instead of writing a fresh implementation, an `expect class` can be satisfied by aliasing an existing platform type: ```kotlin // commonMain expect class AtomicRef<T>(initial: T) { fun get(): T } // jvmMain actual typealias AtomicRef<T> = java.util.concurrent.atomic.AtomicReference<T> ``` The platform type must already provide the expected members. ## Where to place actuals Put the `expect` in `commonMain`. Put each `actual` in the **most-specific source set** that covers the targets needing the same implementation: - One platform → that platform's set (`jvmMain`). - Several related platforms with shared code → an **intermediate** set such as `appleMain` or `nativeMain`, so one `actual` serves them all. ## Compiler enforcement The compiler requires **every** declared target reachable from the `expect` to have a matching `actual`. Add a new target, and any unmatched `expect` becomes a build error until you supply the `actual`. Signatures (parameter types, return type, name) must match exactly. ## When NOT to use expect/actual Expect/actual is the heaviest tool. For simple cases prefer an ordinary interface in common with platform implementations injected, or just a regular function passed in — reserve `expect/actual` for declarations that genuinely must resolve to a platform type at compile time.

  • What happens if you add iosArm64() but forget its actual?
    The build fails: every expect must have a matching actual for each target it compiles to, so the missing iOS actual is a compile error.
  • When is an interface in common better than expect/actual?
    When you don't need compile-time mapping to a platform type — an interface plus injected platform implementations is simpler and more testable.

expect is a job description posted at HQ; each office must hire an actual person (actual) who fits it — leave one office unstaffed and the build refuses to ship.

saying these in an interview costs you the question

  • Putting expect in a platform source set instead of commonMain
  • Thinking a missing actual is only a runtime error
  • Not knowing actual typealias exists
  • Reaching for expect/actual when a plain interface would do

context