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.
answer
- Alias = same type, structurally checked
- No type safety, ever
- Both expand to String -> interchangeable
- value class for real distinction
- Primitive obsession not solved by aliases
basics
~20 sNo. 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 sNo — 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 linestypealias 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 typesgo deeper
Recognizes that both aliases are just String and therefore mixable.
Explains structural equivalence and names inline value class as the fix.
Discusses the readability-vs-safety trade-off and inlining/boxing nuances of value classes.
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