skip to content

How do expect/actual declarations work, and what are their rules and limits?

level: middleimportance: must knowfreq 65%

answer

  1. expect = promise in common, actual = fill per target
  2. Every target must supply a matching actual
  3. expect class can map to actual typealias
  4. Defaults live on expect side only
  5. Prefer interface + DI for big abstractions

basics

~10 s

expect/actual is how common code says 'something with this shape exists' and each platform fills in the real version. Common code declares expect; every target must provide a matching actual.

solid answer

~40 s

In `commonMain` you write an `expect` declaration — a function, property, class, object, or typealias with a signature but no body. Each platform source set (e.g. `androidMain`, `iosMain`) must provide a matching `actual` with the same name and signature. The compiler checks that every target supplies an actual; missing or mismatched actuals fail compilation. `expect class` can map to an `actual typealias` pointing at an existing platform type (common for wrapping `java.util.UUID` or `NSDate`). Limits: actuals can add members but the visible shape must match; default parameter values go only on the expect side; expect declarations can't have bodies. Modern guidance favors interfaces + dependency injection over heavy expect class usage, reserving expect/actual for thin platform bindings.

code

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

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

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

go deeper

for a junior

Knows expect is declared in common and actual is provided per platform.

for a middle

Explains the compile-time enforcement, expect class → actual typealias, and where defaults live.

for a senior

Weighs expect/actual vs interface+DI, cites coupling/testability tradeoffs, and handles typealias bindings to platform types.

for a principal

Sets team conventions limiting expect/actual to leaf bindings and anticipates refactor cost and the relaxed-rules direction of Kotlin.

## The problem expect/actual solves Common code can only see multiplatform APIs. When it needs something a single platform provides (time, random/UUID, file paths, secure storage, logging), you need a way to declare the shape in common and bind the implementation per platform. That mechanism is **`expect`/`actual`**. ## How it works - In **`commonMain`**, mark a declaration **`expect`**. It has a signature but **no body** — it's a promise. - In **each** target source set, provide an **`actual`** with the **same name and matching signature**. - The compiler verifies every target has an actual; a missing or mismatched one is a **compile error**. ```kotlin // commonMain expect class Platform() { val name: String } expect fun randomUuid(): String // androidMain actual class Platform actual constructor() { actual val name: String = "Android" } actual fun randomUuid(): String = java.util.UUID.randomUUID().toString() // iosMain actual class Platform actual constructor() { actual val name: String = "iOS" } actual fun randomUuid(): String = platform.Foundation.NSUUID().UUIDString() ``` ## What can be expect/actual - **Functions** and **top-level properties** - **Classes**, **interfaces**, **objects**, **enums**, **annotations** - **`expect class` → `actual typealias`**: a powerful shortcut where the actual is just an alias to an existing platform type: ```kotlin // commonMain expect class AtomicInt(value: Int) { fun incrementAndGet(): Int } // jvmMain actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger ``` ## Rules and gotchas - The **expect** side carries **default parameter values**; the `actual` must not repeat them. - An `actual class` may add extra members, but everything the expect declares must be present with matching visibility/signature. - `expect` declarations **cannot have bodies** (one nuance: `expect`/`actual` is being relaxed in newer Kotlin so a common implementation can be the default — but the classic rule is no body). - **Annotation propagation**: some annotations must be repeated on actuals. ## When NOT to use it Heavy `expect class` hierarchies create tight coupling and awkward refactors. Idiomatic modern KMP prefers a plain **interface in commonMain + per-platform implementations injected via DI**, reserving `expect/actual` for thin, leaf-level platform bindings.

  • What happens if one target has no actual for an expect?
    Compilation fails for that target — the compiler enforces an actual in every target that includes the source set.
  • Why might you prefer an interface over expect class?
    Interfaces with DI are easier to test, mock, and refactor, and avoid the tight coupling of an expect class spread across source sets.

expect is a job posting with a spec; each platform hires its own employee (actual) that matches the spec.

saying these in an interview costs you the question

  • Putting a body on an expect declaration
  • Thinking only one platform needs an actual
  • Repeating default parameter values on the actual side
  • Using expect class for everything instead of interfaces + DI
  • Believing actual must match expect exactly with no extra members

context