skip to content

When designing a shared `expect class`, how do you decide between satisfying it with an `actual typealias` versus an `actual class`? Walk through the trade-offs.

level: seniorimportance: should knowfreq 28%

answer

  1. alias = free + interop, but leaks + fragile
  2. class = control + parity, but boilerplate + overhead
  3. wrap when you must normalize or hide
  4. alias when platform type is a clean superset
  5. can mix per target

basics

~20 s

Use actual typealias when a platform type already matches the contract exactly — it's free and gives perfect interop. Use actual class when you need to add, rename, normalize, or hide members, or no single platform type fits.

solid answer

~50 s

Pick `actual typealias` when an existing platform type is a clean superset of the `expect` surface: you get zero wrapper code, seamless interop (Java libraries returning that type plug straight in), and no allocation overhead. Pick `actual class` when (a) no platform type covers the contract, (b) you must normalize behavior across targets (consistent `toString`, equality, error handling), (c) you want to *hide* the platform type so it can't leak, or (d) you need extra members or a different constructor shape. The alias couples your contract to the platform type's exact API and leaks its full surface into platform code; the class gives you a controlled boundary at the cost of delegation boilerplate and possible boxing. A common pattern is `actual class` that internally holds the platform type and delegates, exposing only the contracted members — encapsulation when you don't trust the raw type's surface.

go deeper

for a junior

Knows alias reuses an existing type and class is written by hand.

for a middle

Lists concrete pros/cons: boilerplate, interop, overhead for each option.

for a senior

Weighs encapsulation, behavioral parity, and contract evolution; knows you can mix per target.

for a principal

Treats the choice as API-boundary governance — balancing interop, portability, and long-term contract stability across the whole codebase.

## The decision in one line **Alias when the platform type already *is* your contract; wrap (class) when you need control.** ## When `actual typealias` wins - A widely-used platform type matches the `expect` surface (e.g. `java.util.UUID`, `java.math.BigDecimal`, `kotlinx.atomicfu` types). - **Interop matters**: Java APIs that produce/consume `java.util.UUID` work with your common `UUID` with no conversions. - **Performance**: no wrapper object, no boxing, no delegation indirection. - **Less code**: members are inherited from the real type; nothing to redeclare. ```kotlin // jvmMain actual typealias BigDecimal = java.math.BigDecimal ``` ## When `actual class` wins - **No matching type** exists, or the surfaces differ (extra members, different constructors). - **Behavioral normalization**: you want identical `toString`/equality/exception semantics across all targets, which the raw platform types won't give you. - **Encapsulation**: you want to *prevent* the platform type from leaking into platform code, so consumers can only touch contracted members. - **Future flexibility**: you may swap the backing implementation later without changing the common contract. ```kotlin // jvmMain — wrap + delegate, surface controlled actual class Money actual constructor(amount: String) { private val backing = java.math.BigDecimal(amount) actual fun plus(other: Money): Money = Money(backing.add(other.backing).toString()) actual override fun toString(): String = backing.toPlainString() // normalized } ``` ## The trade-off table | Concern | `actual typealias` | `actual class` | |---|---|---| | Boilerplate | none | delegation per member | | Interop with platform libs | perfect | needs conversion | | Overhead | zero | wrapper object/boxing | | Encapsulation (hide platform type) | none — full leak | full control | | Behavioral parity across targets | not guaranteed | you enforce it | | Contract evolution safety | fragile (tied to platform API) | robust (you implement) | ## Rule of thumb Start with the alias for ubiquitous, stable types. Switch to a wrapping `actual class` the moment you need normalization, hiding, or members the platform type lacks. Mixing is fine: alias on JVM where the type exists, wrap on Native where it doesn't.

  • Can you use an alias on one target and a wrapping class on another for the same expect?
    Yes. Each target independently provides its actual; aliasing on JVM and writing an `actual class` on Native is a common, legitimate pattern.
  • What's the main downside of always wrapping with `actual class`?
    Delegation boilerplate, possible allocation/boxing overhead, and lost interop — Java APIs returning the platform type now need conversion to your wrapper.

saying these in an interview costs you the question

  • Always wrapping even when a perfect platform type exists
  • Aliasing when behavior must be normalized across targets
  • Ignoring interop cost of a wrapper class
  • Not realizing you can mix alias and class per target
  • Claiming aliasing guarantees cross-platform behavioral parity

context