skip to content

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?

level: seniorimportance: should knowfreq 35%

answer

  1. typealias is transparent → no protection
  2. Same shape = same type
  3. fun interface for distinct callback types
  4. @JvmInline value class for distinct values
  5. Structural (alias) vs nominal (class)

basics

~20 s

The 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 s

Typealiases 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
kotlin
// 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 wanted

go deeper

for a junior

Recognizes the aliases are interchangeable and offer no safety.

for a middle

Explains transparency and names fun interface / value class as alternatives.

for a senior

Frames it as structural vs nominal typing and picks the right tool per use case with runtime-cost awareness.

for a principal

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

context