The Case of the Missing Handler
The hidden cost of using typealias with lambdas

Search for a command to run...
The hidden cost of using typealias with lambdas

No comments yet. Be the first to comment.
Kotlin's syntactic sugar: delightful but dangerous? "Syntactic Sours" explores how overuse in large codebases can hinder readability, complicate debugging, and introduce subtle bugs.
Introduction: The Scene of the Crime It was a dark and stormy night. My hands were flying across the keys when suddenly the codebase began to exhibit strange behavior. Hashes, which once returned the same values for identical objects, suddenly became...
A Frictionless Approach to Self-Advocacy for Engineers and Managers

A best practice to prevent feature flag drift.

A Recipe for Great Engineering Demos

Wait... didn't we just compose this?

Ask anyone who worked at Block/Square what they loved most and I guarantee you the first thing they'll say is "its the people". Square's mission of economic empowerment and growing by helping others g
Our story begins with a clean line of Kotlin:
typealias Handler = (result: Result) -> Unit
It looks innocent. Give a function type a name and tidy up the signatures, great!
Then a bug hits. The Handler is gone in the debugger and in stack traces only (Result) -> Unit remains. Our alias has disappeared at the scene of the crime.
That is the trick, a typealias does not create a new type. It is only a name for the function type.
fun interface Handler {
fun handle(result: Result)
}
Now the Handler exists. It has identity, it can carry documentation, and the method name handle shows up in stack traces and search results. Usage is still light:
val handler: Handler = Handler { result ->
println(result)
}
Aliases collapse to the same function type, which prevents overloading:
typealias Handler = (Result) -> Unit
typealias Completion = (Result) -> Unit
fun process(h: Handler) { }
fun process(c: Completion) { } // Won't compile, same JVM signature.
Functional interfaces are distinct types, so overloads compile:
fun interface Handler {
fun handle(result: Result)
}
fun interface Completion {
fun complete(result: Result)
}
fun process(h: Handler) { }
fun process(c: Completion) { }
Using a single alias for several callbacks in the same type invites mistakes. The compiler cannot help if you swap them. It also won’t tell you which was called while debugging or looking at a stacktrace.
// One alias for two different callbacks
typealias Handler = (Result) -> Unit
data class Screen(
val onClick: Handler,
val onDismiss: Handler
)
val click: Handler = { println("clicked: $it") }
val dismiss: Handler = { println("dismissed: $it") }
// Compiles, but the handlers are reversed
val screen = Screen(onClick = dismiss, onDismiss = click)
With functional interfaces you can give each callback a distinct type, so it is impossible to construct the Screen incorrectly and stacktraces name the method you expect.
fun interface ClickHandler { fun onClick(result: Result) }
fun interface DismissHandler { fun onDismiss(result: Result) }
data class Screen(
val onClick: ClickHandler,
val onDismiss: DismissHandler
)
// Will not compile if you swap types
// val screen = Screen(onClick = DismissHandler { ... }, onDismiss = ClickHandler { ... })
val screen = Screen(
onClick = ClickHandler { println("clicked: $it") },
onDismiss = DismissHandler { println("dismissed: $it") }
)
You can put kdoc on a functional interface and it surfaces in the IDE. You can also add small helpers as extensions:
/** Called once per operation with the final result. */
fun interface Handler {
fun handle(result: Result)
}
fun Handler.logged(): Handler = Handler { result ->
Log.d("Handler", "Handled: $result")
this.handle(result)
}
With a typealias, the alias name is not present. You will usually see $lambda$ or Function1:
java.lang.IllegalStateException: boom in typealias
at FileKt.main$lambda$0(File.kt:27) // What was called here??
at FileKt.main(File.kt:30)
at FileKt.main(File.kt)
With a functional interface, the interface and its method are visible, which makes searching and triage simpler:
java.lang.IllegalStateException: boom in fun interface
at LoggingHandler.handle(File.kt:18) // Clear name of the caller and override!
at FileKt.main(File.kt:38)
at FileKt.main(File.kt)
This will also happen in the call stack in the debugger leaving you stranded as you attempt to navigate through your breakpoints.
A named type makes simple fakes straightforward:
val calls = mutableListOf<Result>()
val handler: Handler = Handler { result -> calls += result }
// exercise code that calls handler.handle(...)
check(calls.isNotEmpty())
typealias makes code look cleaner but they do not provide type identity. If you want names that survive code navigation, debugging, and evolution, prefer a functional interface.
typealias and fun interface to capture the stack trace to see the difference in the output!typealias?When dealing other cases that aren’t lambdas, especially with complicated or nested generic types. They are great for turning complex types into readable, reference-able, and remember-able types!