Since a top-level `typealias` is a structural synonym, what subtle pitfalls can it introduce around overload resolution, `when`/`is` checks, and API evolution? How would you set policy for using typealiases in a large codebase?
answer
- No identity -> can't overload on alias
- is/as/when see underlying type only
- Reflection reports underlying type
- Retargeting alias = silent breaking change
- Policy: readability only, value class for safety
basics
~20 sBecause an alias is the same type, you can't overload on it, you can't tell it apart with is checks, and renaming what it points to silently changes meaning everywhere. So use aliases only for readability and treat them as throwaway names, not type boundaries.
solid answer
~50 sA `typealias` is a compile-time synonym, so it offers **no distinct identity** the compiler or runtime can key on. Consequences: you **cannot overload** two functions that differ only by alias vs underlying type (e.g. `f(x: UserId)` and `f(x: String)` clash — same signature after expansion); `is`/`as`/`when` checks see only the **underlying** type (`x is UserId` reduces to `x is String`), so they can't discriminate aliases; reflection and `::class` report the underlying type. For **API evolution**, changing what an alias points to (`typealias Id = Long` -> `= UUID`) silently propagates everywhere the name is used — powerful but easy to break call sites or serialization. Policy I'd set: use aliases for **readability of verbose generic/function types** and disambiguation, keep public-API aliases `internal`/`private` unless intentionally part of the vocabulary, **never** rely on them for safety (use `inline value class` for distinct domain types), and document that aliases are presentation-only. Pair with lint rules to catch attempts to overload on aliases or treat them as nominal.
code
kotlin · 16 linestypealias UserId = String
// 1) Overload clash:
// fun handle(x: UserId) {}
// fun handle(x: String) {} // ERROR: conflicting overloads
// 2) is-check can't discriminate:
typealias Meters = Double
fun describe(v: Any) = when (v) {
is Meters -> "meters" // same as `is Double`
else -> "other"
}
fun main() {
println((3.0 as Any)::class.simpleName) // "Double" — never "Meters"
}go deeper
Knows an alias is the same type, so it behaves identically to the original in checks.
Identifies that you can't overload on an alias and that is checks see the underlying type.
Explains the API-evolution risk of retargeting and reflection seeing the underlying type, and prefers value classes for safety.
Sets codebase policy: readability-only aliases, tight scoping, breaking-change review for retargets, and lint guards against nominal misuse.
## Root cause: no distinct identity After expansion, an alias **is** its underlying type. Nothing — compiler, runtime, reflection — can distinguish `Alias` from `Underlying`. Every pitfall below follows from this. ## Pitfall 1 — overload resolution clashes You cannot disambiguate overloads by alias: ```kotlin typealias UserId = String fun handle(x: UserId) {} fun handle(x: String) {} // ERROR: conflicting overloads (same signature after expansion) ``` Because both signatures expand to `handle(String)`, they are duplicates. ## Pitfall 2 — `is` / `as` / `when` can't discriminate ```kotlin typealias Meters = Double val v: Any = 3.0 when (v) { is Meters -> ... // identical to `is Double` is Double -> ... // unreachable duplicate } ``` Type tests reduce to the underlying type; the alias name carries no runtime tag. Likewise `::class` / reflection report the underlying type (`Double`, `String`), never the alias. ## Pitfall 3 — silent API evolution Changing the alias target rewrites meaning everywhere at once: ```kotlin typealias Id = Long // v1 // later: typealias Id = java.util.UUID // v2 — every Id usage changes type silently ``` This is convenient for sweeping refactors but dangerous: serialization formats, equals/hashCode behavior, and downstream call sites can break with **no local diff** at the use sites. For a published library, the underlying type is what consumers actually bind to, so an alias retarget is a **breaking change** even though the alias name is unchanged. ## Pitfall 4 — false sense of safety / primitive obsession Teams sometimes adopt aliases hoping for domain safety; they get none (see sibling: inline value class). This can entrench primitive obsession while *looking* type-safe. ## What aliases are genuinely good for - Shortening verbose generics: `typealias Handlers = Map<EventType, List<(Event) -> Unit>>`. - Naming function types for readable signatures. - Disambiguating clashing imported names within a file/module. ## Policy for a large codebase 1. **Readability only.** Treat aliases as presentation; never as a type boundary or safety mechanism. 2. **Use `inline value class` for distinct domain identifiers** (UserId vs OrderId). 3. **Scope tightly.** Prefer `internal`/`private` for aliases unless they are a deliberate part of the public vocabulary, so retargeting stays a local concern. 4. **Treat public-API alias retargets as breaking changes** — review them like signature changes. 5. **Lint/review rules:** flag overload attempts that differ only by alias, and `is`/`when` branches that test an alias as if nominal. 6. **Document** each alias's underlying type at the declaration so readers reason about the real type. ## Summary Aliases buy readability at the cost of any nominal distinction; great for taming verbose types, wrong for safety, and quietly impactful during evolution. Govern them as cosmetic, module-scoped helpers and reach for value classes when identity matters.
- Can two overloads differ only by `typealias Id = String` vs `String`?No — they expand to the same signature and conflict. Overloads need genuinely different parameter types.
- Is changing what a public-API typealias points to a breaking change?Yes. Consumers bind to the underlying type, so retargeting silently changes the contract — review it like any signature change.
- Can reflection recover the alias name at runtime?No. `::class` and reflection report the underlying type; the alias exists only at compile time.
An alias is a stage name: scripts read nicely, but the actor's passport (the real type) is what every legal check uses — rename roles freely, identity never changes.
saying these in an interview costs you the question
- Proposing overloads distinguished only by an alias
- Using `is Alias` expecting runtime discrimination
- Retargeting a public alias without treating it as breaking
- Recommending aliases for domain type safety
- Believing reflection can see the alias name