In Kotlin, type aliases let you give a shorter, more meaningful name to an existing type. They are especially useful when your code has deeply nested generics, function types, or repeated complex class structures.
A type alias does not create a new type. It only creates an alternative name for an existing type.
Basic syntax
typealias AliasName = ExistingType
Example:
typealias UserId = String
fun findUser(id: UserId) {
println("Finding user with id: $id")
}
Here, UserId is still just a String, but the code is more expressive.
Simplifying complex generic types
Suppose you have a complex nested structure:
val permissions: Map<String, List<Pair<String, Boolean>>> = mapOf(
"admin" to listOf("delete" to true, "edit" to true),
"guest" to listOf("view" to true)
)
You can simplify it with type aliases:
typealias PermissionName = String
typealias IsAllowed = Boolean
typealias Permission = Pair<PermissionName, IsAllowed>
typealias RolePermissions = Map<String, List<Permission>>
val permissions: RolePermissions = mapOf(
"admin" to listOf("delete" to true, "edit" to true),
"guest" to listOf("view" to true)
)
This makes the purpose of each part clearer.
Simplifying nested class structures
Type aliases are useful when referencing nested classes:
class ApiResponse {
class Metadata {
class Pagination {
data class PageInfo(
val page: Int,
val size: Int,
val total: Int
)
}
}
}
typealias PageInfo = ApiResponse.Metadata.Pagination.PageInfo
fun printPageInfo(info: PageInfo) {
println("Page ${info.page} of size ${info.size}")
}
Instead of writing:
ApiResponse.Metadata.Pagination.PageInfo
everywhere, you can use:
PageInfo
Simplifying function types
Type aliases are very common for callbacks and handlers:
typealias SuccessCallback<T> = (T) -> Unit
typealias ErrorCallback = (Throwable) -> Unit
fun <T> loadData(
onSuccess: SuccessCallback<T>,
onError: ErrorCallback
) {
try {
// Load data
} catch (e: Throwable) {
onError(e)
}
}
This is easier to read than:
fun <T> loadData(
onSuccess: (T) -> Unit,
onError: (Throwable) -> Unit
)
Simplifying collection-heavy models
For example, instead of repeatedly writing:
Map<String, MutableList<Map<String, Any?>>>
you can define:
typealias JsonObject = Map<String, Any?>
typealias JsonObjectList = MutableList<JsonObject>
typealias GroupedJsonObjects = Map<String, JsonObjectList>
Then use:
fun process(data: GroupedJsonObjects) {
// ...
}
Type aliases with generic parameters
Type aliases can also be generic:
typealias ResultHandler<T> = (Result<T>) -> Unit
fun fetchUser(handler: ResultHandler<User>) {
// ...
}
Another example:
typealias StringMap<T> = Map<String, T>
val userAges: StringMap<Int> = mapOf(
"Alice" to 30,
"Bob" to 25
)
Important limitation: aliases are not new types
This is valid:
typealias UserId = String
typealias ProductId = String
fun loadUser(id: UserId) {
println(id)
}
val productId: ProductId = "p-123"
loadUser(productId)
Even though ProductId and UserId have different alias names, both are still String.
If you need real type safety, use a value class instead:
@JvmInline
value class UserId(val value: String)
@JvmInline
value class ProductId(val value: String)
Now UserId and ProductId are distinct types.
When to use type aliases
Use type aliases when you want to:
- Shorten long generic types
- Give semantic names to data structures
- Improve readability of callback/function types
- Simplify references to nested classes
- Avoid repeating verbose type declarations
Avoid using them when:
- You need a truly distinct type
- The alias hides important complexity
- The alias name is vague, like
Data,Info, orThing
Example: before and after
Before:
class EventBus {
private val listeners: MutableMap<String, MutableList<(Map<String, Any?>) -> Unit>> =
mutableMapOf()
fun subscribe(event: String, listener: (Map<String, Any?>) -> Unit) {
listeners.getOrPut(event) { mutableListOf() }.add(listener)
}
fun publish(event: String, payload: Map<String, Any?>) {
listeners[event]?.forEach { listener ->
listener(payload)
}
}
}
After:
typealias EventName = String
typealias EventPayload = Map<String, Any?>
typealias EventListener = (EventPayload) -> Unit
typealias ListenerRegistry = MutableMap<EventName, MutableList<EventListener>>
class EventBus {
private val listeners: ListenerRegistry = mutableMapOf()
fun subscribe(event: EventName, listener: EventListener) {
listeners.getOrPut(event) { mutableListOf() }.add(listener)
}
fun publish(event: EventName, payload: EventPayload) {
listeners[event]?.forEach { listener ->
listener(payload)
}
}
}
The runtime behavior is the same, but the structure is easier to understand.
In short: use typealias to make complex Kotlin types easier to read, but use value classes when you need stronger type safety.
