skip to content

Your common `expect class UUID` is satisfied on JVM by `actual typealias UUID = java.util.UUID`. What surprising consequences does this aliasing have for common code consumers and for other targets?

level: seniorimportance: should knowfreq 30%

answer

  1. JVM sees full JDK API, common sees only expect
  2. no new type → leakage into platform code
  3. each target needs its own actual
  4. structural contract, not behavioral parity
  5. evolving expect can break the alias

basics

~20 s

On JVM, UUID literally is java.util.UUID, so its full Java API leaks into JVM-side code, and common code must still only use the members the expect declares. Other targets need their own actual, which may behave differently.

solid answer

~50 s

Because the alias makes `UUID === java.util.UUID` on JVM, anything in jvmMain (or Java callers) sees the *entire* `java.util.UUID` API — `getMostSignificantBits()`, `version()`, etc. — even though commonMain only sees the narrow `expect` surface. That asymmetry is intentional but can surprise: code written against common compiles everywhere, yet platform code can lean on extra members and accidentally couple to the JDK. Each other target must supply its own actual: Native/JS have no `java.util.UUID`, so you alias to a different type or write an `actual class`, and subtle differences (string formatting, equality, thread-safety) can diverge per platform. Also, an aliased type can't be `expect`-side `sealed`/`data` in ways the platform type doesn't honor, and adding a member to the `expect` later can break the JVM alias if `java.util.UUID` lacks it. So the alias is great for interop but pushes you to keep the `expect` surface minimal and the cross-platform semantics aligned.

code

kotlin · 9 lines
kotlin
// commonMain
expect class UUID { override fun toString(): String }

// jvmMain
actual typealias UUID = java.util.UUID
fun jvmOnly(u: UUID): Long = u.mostSignificantBits  // JDK extra, fine here

// commonMain — won't compile: member not in expect surface
// fun common(u: UUID): Long = u.mostSignificantBits

go deeper

for a junior

Recognizes JVM UUID is the JDK type and other targets need their own actual.

for a middle

Explains the visibility split between common and platform code and that each target supplies an actual.

for a senior

Reasons about leakage, behavioral non-parity, and fragile contract evolution as design trade-offs.

for a principal

Sets policy: minimal expect surfaces, parity tests per target, alias-vs-wrap decisions framed by long-term portability and API stability.

## The core asymmetry A typealias creates **no new type**, so on JVM `UUID` is *identically* `java.util.UUID`. This produces a deliberate visibility split: - **commonMain** sees only the members the `expect class UUID` declares. - **jvmMain / Java interop** sees the **full** `java.util.UUID` API, because there the name resolves to the real JDK class. ```kotlin // commonMain — only expect members visible fun id(): UUID = UUID.randomUUID() // ok // fun bits(u: UUID) = u.mostSignificantBits // would NOT compile in common // jvmMain — the whole JDK API is available fun bits(u: UUID): Long = u.mostSignificantBits // ok: u IS java.util.UUID ``` ## Consequence 1 — platform leakage Platform code can use members beyond the contract. That's powerful (full interop) but risks coupling logic to JDK-only behavior, undermining portability if that logic later needs to move to common. ## Consequence 2 — every target needs its own actual Native, JS, and Wasm have no `java.util.UUID`. For each you must either alias to a different existing type or write an `actual class`: ```kotlin // nativeMain — no JDK; implement or alias to something else actual class UUID actual constructor(/*...*/) { /* ... */ } ``` Differences in string form, equality, ordering, or thread-safety can diverge across targets unless you deliberately align them. The `expect` is only a *structural* contract — it does **not** guarantee identical runtime behavior. ## Consequence 3 — fragile contract evolution Add a new method to `expect class UUID` and the JVM alias keeps compiling **only if** `java.util.UUID` already has a matching member. Otherwise the alias breaks and you must convert to an `actual class`. So aliases couple your common contract to whatever the platform type happens to expose. ## Consequence 4 — modifiers and identity The aliased type's nature (final/open, `equals`/`hashCode`, serialization) is whatever the platform type defines. You can't make `java.util.UUID` `data` or `sealed` via the alias; common code must not assume features the platform type doesn't provide. ## Design takeaways - Keep the `expect` surface **minimal** — every member is a constraint on all platforms. - Treat behavioral parity (formatting, equality) as something you test per target, not something the alias guarantees. - Prefer aliases for stable, ubiquitous platform types (UUID, BigDecimal); wrap when behavior must be normalized.

  • If common code only sees the expect members, why does platform leakage matter?
    Because platform modules can build logic on JDK-only members, that logic becomes non-portable and can't be lifted into common later without rework.
  • Does the `expect`/`actual` mechanism guarantee identical behavior across targets?
    No. It only guarantees a matching structural surface. Formatting, equality, ordering, and thread-safety can differ; you must test parity yourself.

The expect is a small window; the JVM alias is a glass wall — platform code sees everything, common code only the window pane.

saying these in an interview costs you the question

  • Assuming common code can call full JDK UUID API
  • Believing expect/actual guarantees identical runtime behavior
  • Thinking one JVM alias covers Native/JS automatically
  • Ignoring that adding an expect member can break the alias
  • Claiming you can make the aliased type `data`/`sealed` via the alias

context