skip to content

A teammate uses `typealias Email = String` and `typealias Phone = String` to make signatures clearer. What problem can this hide, and how would a value class fix it?

level: middleimportance: must knowfreq 55%

answer

  1. Aliases of String are all the same type → swap bug compiles
  2. Value class = distinct type → swap is a compile error
  3. init { require(...) } adds validation aliases can't
  4. Wrap at construction, unwrap via .value at boundaries
  5. Primitive obsession: name vs enforced identity

basics

~20 s

Because both aliases are just String, the compiler treats them as the same type. You can accidentally pass a phone number where an email is expected and nothing complains. A value class makes them separate types, so the compiler catches the mix-up.

solid answer

~50 s

`typealias Email = String` and `typealias Phone = String` are *transparent synonyms*: both are exactly `String`, so they are mutually assignable and the compiler offers **no protection** against swapping them. `register(email: Email, phone: Phone)` can be called with the arguments reversed and it compiles fine — the aliases improved readability but added zero safety. Replacing them with `@JvmInline value class Email(val value: String)` and `@JvmInline value class Phone(val value: String)` makes them **distinct types**: you must wrap (`Email("[email protected]")`) and cannot pass a `Phone` where an `Email` is required, so the argument-swap bug becomes a compile error. You also get a place to add `init { require(...) }` validation. The cost is explicit wrap/unwrap at boundaries (e.g., `email.value`), and the usual boxing caveats. The lesson: aliases document intent but don't enforce it; value classes enforce it at (mostly) zero runtime cost.

go deeper

for a junior

Recognizes that two String aliases are interchangeable and a swap can go unnoticed.

for a middle

Shows the concrete swap bug and converts to value classes for compile-time safety plus validation.

for a senior

Weighs wrap/unwrap ergonomics, serialization impact, and boxing against the safety gained.

for a principal

Frames it as primitive-obsession remediation and sets a team policy for when identity must be enforced vs merely named.

## The hidden problem A `typealias` introduces **no new type** — it is a *transparent synonym*. So: ```kotlin typealias Email = String typealias Phone = String fun register(email: Email, phone: Phone) { /* ... */ } val e = "[email protected]" val p = "+123" register(p, e) // COMPILES — arguments swapped, no error! ``` Both `Email` and `Phone` collapse to `String`, which is also the type of any random `String` you have lying around. The aliases read nicely but provide **no compile-time guarantee** that the right kind of string reaches the right parameter. This is a classic *primitive-obsession* trap dressed up to look safe. ## The value-class fix ```kotlin @JvmInline value class Email(val value: String) { init { require('@' in value) { "invalid email" } } } @JvmInline value class Phone(val value: String) fun register(email: Email, phone: Phone) { /* ... */ } val e = Email("[email protected]") val p = Phone("+123") register(e, p) // OK // register(p, e) // COMPILE ERROR — Phone is not Email ``` Now `Email` and `Phone` are **distinct types**. The swap is impossible without an explicit (and visibly wrong) re-wrap. Bonus benefits: - **Validation** in `init { require(...) }` so an `Email` can never hold a non-email string. - **Domain methods** like `val Email.domain get() = value.substringAfter('@')`. - **Zero-cost**: the single `val` is inlined, so in direct use no wrapper object is allocated. ## Trade-offs to acknowledge - You must **wrap at construction** and **unwrap** (`email.value`) when you need the raw `String` (e.g., for a JSON library or SQL parameter). Serialization frameworks may need a custom serializer/converter. - The usual **boxing** situations apply (nullable, generics, interfaces). - Slightly more ceremony than a bare alias. ## Rule of thumb Use a `typealias` when you only want a shorter/clearer **name** and interchangeability is fine. Use a `value class` when the type identity must be **enforced** — anywhere mixing two same-underlying values would be a bug.

  • What extra capability does the value class give beyond preventing the swap?
    Construction-time validation via `init { require(...) }`, plus domain methods/properties, so the invariant lives with the type.
  • What is the main ergonomic cost of switching from alias to value class?
    You must explicitly wrap on creation and unwrap (`.value`) for raw-String boundaries like serialization or SQL, and may need custom serializers.

Two nicknames for the same person can be confused; two sealed, labeled boxes cannot be handed to the wrong door.

saying these in an interview costs you the question

  • Believing typealias somehow prevents passing the wrong string
  • Saying aliases add validation
  • Not mentioning explicit wrap/unwrap cost of value classes
  • Claiming value class is a runtime-heavy wrapper for this case
  • Suggesting a data class with one field instead (allocates, heavier)

context