A teammate uses `typealias UserId = (String) -> Unit` and `typealias OrderId = (String) -> Unit` expecting the compiler to keep them apart. What actually happens, and how should they get the safety they want?
answer
- typealias is transparent → no protection
- Same shape = same type
- fun interface for distinct callback types
- @JvmInline value class for distinct values
- Structural (alias) vs nominal (class)
basics
~20 sThe compiler treats both as the same type, so it won't keep them apart — you can freely swap them. To get real distinct types, use separate classes, value classes, or fun interfaces instead of aliases.
solid answer
~40 sTypealiases are **transparent**: the compiler erases them to the underlying type before type checking, so two aliases of the same shape are the *same* type and freely interchangeable. The teammate gets no protection — passing a `UserId` callback where an `OrderId` is expected compiles fine. This is by design: typealias is a readability/documentation tool, not a nominal-typing mechanism. For real distinctness use a **`fun interface`** (distinct SAM type) when you need a callback type, or wrap values in a **`value class`** (`@JvmInline value class UserId(val raw: String)`) for distinct *data* types. Both create nominal identity the compiler enforces, with value classes typically inlined to avoid allocation. The general rule: typealias when you want structural convenience; a real type (class/value class/fun interface) when you want the compiler to stop accidental mixing.
code
kotlin · 12 lines// Aliases give NO safety:
typealias UserId = (String) -> Unit
typealias OrderId = (String) -> Unit
fun handleUser(u: UserId) {}
val order: OrderId = { }
handleUser(order) // compiles — interchangeable
// Distinct nominal types do:
fun interface UserIdHandler { fun handle(id: String) }
fun interface OrderIdHandler { fun handle(id: String) }
fun handle(u: UserIdHandler) {}
// handle(OrderIdHandler { }) // compile error — exactly the safety wantedgo deeper
Recognizes the aliases are interchangeable and offer no safety.
Explains transparency and names fun interface / value class as alternatives.
Frames it as structural vs nominal typing and picks the right tool per use case with runtime-cost awareness.
Establishes a codebase policy: aliases for readability, value classes/fun interfaces for domain-type safety, and reviews APIs for accidental structural mixing.
## What actually happens ```kotlin typealias UserId = (String) -> Unit typealias OrderId = (String) -> Unit fun handleUser(u: UserId) {} val order: OrderId = { id -> println(id) } handleUser(order) // COMPILES — no error! ``` A **typealias is transparent**: the compiler substitutes the underlying type before type checking. So `UserId` and `OrderId` are both literally `(String) -> Unit` — the *same* type. There is no nominal identity, hence **zero protection** against swapping them. This is intentional: typealias exists for **readability/documentation**, not type safety. ## How to get real distinctness ### Option 1 — `fun interface` (for callback types) A **`fun interface`** (Single Abstract Method interface) is a **distinct nominal type**. Lambdas convert to it via **SAM conversion**, but two such interfaces with identical signatures are not interchangeable. ```kotlin fun interface UserIdHandler { fun handle(id: String) } fun interface OrderIdHandler { fun handle(id: String) } fun handleUser(u: UserIdHandler) {} val order = OrderIdHandler { println(it) } // handleUser(order) // does NOT compile — different types handleUser { id -> println(id) } // a lambda still works via SAM conversion ``` ### Option 2 — `value class` (for distinct *values*, not callbacks) If the real intent is distinct *identifiers* (data), an inline **`value class`** is the idiomatic answer: ```kotlin @JvmInline value class UserId(val raw: String) @JvmInline value class OrderId(val raw: String) fun lookup(id: UserId) {} // lookup(OrderId("x")) // does NOT compile ``` `@JvmInline value class` wraps a single value, gives a distinct compiler-enforced type, and is usually **inlined** at runtime (no wrapper allocation in the common case), so you get safety without overhead. ## Choosing | Goal | Use | |------|-----| | Readable name, structural compatibility OK | `typealias` | | Distinct *callback* type | `fun interface` | | Distinct *value/identifier* type | `@JvmInline value class` | ## The mental model Think **structural vs nominal typing**. Typealias = structural (same shape ⇒ same type). Classes/value classes/fun interfaces = nominal (the *name* defines identity, so same shape ⇒ different types). The teammate wants nominal typing; typealias can't provide it.
- Why does Kotlin make typealias transparent instead of nominal?It is positioned as a documentation/readability tool; transparency keeps it cost-free and interoperable with the underlying type. Nominal typing is provided by classes/value classes/fun interfaces.
- Would a `value class` wrapping a function give distinct callback types?Yes — a value class wrapping a `(String)->Unit` is a distinct nominal type, though invoking it needs an explicit method/operator and it loses direct SAM-from-lambda convenience compared to a fun interface.
Two nicknames for the same person — calling them 'Boss' or 'Sam' doesn't make them two different people.
saying these in an interview costs you the question
- Believing two same-shape aliases are kept apart by the compiler
- Recommending typealias to enforce domain type safety
- Not knowing fun interface / value class as the proper tools
- Confusing structural vs nominal typing
- Thinking value classes always allocate a wrapper at runtime