skip to content

A teammate adds `typealias UserId = String` and `typealias OrderId = String` hoping to prevent mixing them up. Will the compiler catch passing an OrderId where a UserId is expected? Explain why.

level: middleimportance: must knowfreq 50%

answer

  1. Alias = same type, structurally checked
  2. No type safety, ever
  3. Both expand to String -> interchangeable
  4. value class for real distinction
  5. Primitive obsession not solved by aliases

basics

~20 s

No. Both aliases are just other names for String, so the compiler sees them as the same type and lets you swap them freely. A typealias does not create a new type, so it cannot stop the mix-up.

solid answer

~50 s

No — the compiler will not catch it. Because `typealias` introduces a **compile-time synonym** and **not** a new type, `UserId`, `OrderId`, and `String` are all the *same* type after expansion. Kotlin checks them **structurally**: identical underlying type means full interchangeability, so an `OrderId` value flows into a `UserId` parameter with no error. To get the intended safety you need a **distinct nominal type**: an `inline value class UserId(val raw: String)` (compiles to the underlying `String` where possible but is a separate type the compiler enforces) or a plain wrapper `class`/`data class`. The alias still has value — for readability and shortening verbose generics — but it adds *zero* type safety. This is the classic 'aliases don't prevent primitive obsession' point: choose a value class when you want enforcement, an alias when you only want a nicer name.

code

kotlin · 15 lines
kotlin
typealias UserId = String
typealias OrderId = String

fun fetchUser(id: UserId) = id

fun main() {
    val order: OrderId = "o-7"
    fetchUser(order)   // compiles: OrderId IS String IS UserId
}

// Real safety:
@JvmInline value class StrongUserId(val raw: String)
@JvmInline value class StrongOrderId(val raw: String)
fun fetch(id: StrongUserId) = id
// fetch(StrongOrderId("o-7"))  // ERROR: distinct nominal types

go deeper

for a junior

Recognizes that both aliases are just String and therefore mixable.

for a middle

Explains structural equivalence and names inline value class as the fix.

for a senior

Discusses the readability-vs-safety trade-off and inlining/boxing nuances of value classes.

for a principal

Sets team guidance on when domain types are mandated vs when an alias suffices, weighing API ergonomics.

## The scenario ```kotlin typealias UserId = String typealias OrderId = String fun fetchUser(id: UserId): User = ... val order: OrderId = "o-7" fetchUser(order) // compiles fine — NO error ``` ## Why the compiler stays silent A `typealias` creates a **compile-time synonym**, not a new type. The compiler **expands** every alias to its underlying type before type-checking: - `UserId` -> `String` - `OrderId` -> `String` So `fetchUser(id: UserId)` is *actually* `fetchUser(id: String)`, and `order: OrderId` is *actually* `order: String`. They are **the same type**, checked **structurally**. There is nothing for the compiler to reject — no nominal distinction exists. ## The general rule > A `typealias` adds **no type safety**. It is a name, not a boundary. This is independent of the underlying type: `typealias Meters = Double` and `typealias Seconds = Double` are equally mixable. ## How to actually get the safety ### Option 1 — `inline value class` (recommended) ```kotlin @JvmInline value class UserId(val raw: String) @JvmInline value class OrderId(val raw: String) fun fetchUser(id: UserId): User = ... // fetchUser(OrderId("o-7")) // COMPILE ERROR: distinct types ``` An `inline value class` is a **genuinely distinct type** that the compiler enforces, while still being represented as the underlying `String` at runtime in most cases (it may **box** when used as a generic type argument, nullable, or where an interface is expected). This is a *sibling* mechanism — the key point here is that it *does* what the alias cannot. ### Option 2 — a wrapper class / data class Always a distinct type, but always allocates an object (no inlining). ## When to still reach for a typealias - Shortening a verbose generic: `typealias Handlers = Map<String, (Event) -> Unit>`. - Documenting intent where mixing is not a real risk. Use it for **readability**, never for **enforcement**.

  • If aliases give no safety, why use them at all?
    Readability and brevity — shortening verbose generic/function types and documenting intent without runtime cost.
  • Does an inline value class always avoid allocation?
    No. It is inlined to the underlying type in many cases but boxes when used as a generic argument, when nullable, or when an interface/supertype is required.

Putting two different sticky-note labels on identical jars: the labels read differently, but anyone can still grab either jar — nothing physically stops a swap.

saying these in an interview costs you the question

  • Claiming the alias names make the values incompatible
  • Saying the compiler errors on OrderId -> UserId
  • Confusing typealias with inline value class semantics
  • Asserting aliases solve primitive obsession
  • Believing different alias names imply different types

context