How do I use type aliases to simplify complex class structures in Kotlin?

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, or Thing

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.