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.

How do I use the this, super, and @ labels for disambiguation in Kotlin class hierarchies?

In Kotlin, this, super, and @ labels are used to disambiguate which receiver or superclass member you mean, especially in nested scopes, inheritance, and inner classes.

1. this: refer to the current receiver

Inside a class, this refers to the current instance of that class.

class User(val name: String) {
    fun printName() {
        println(this.name)
    }
}

Usually this is optional:

println(name)

is the same as:

println(this.name)

2. this@Label: choose a specific outer receiver

When you have nested classes, lambdas, or extension functions, there may be multiple possible this receivers. Kotlin lets you qualify this with a label.

Class receiver

class Outer {
    val name = "Outer"

    inner class Inner {
        val name = "Inner"

        fun printNames() {
            println(this.name)        // Inner
            println([email protected])  // Inner
            println([email protected])  // Outer
        }
    }
}

this@Outer explicitly means “the this of Outer”.


3. Labels in lambdas

You can label lambdas and then use this@label to access that lambda’s receiver.

class Html {
    fun body() {
        println("body")
    }
}

fun html(block: Html.() -> Unit) {
    Html().block()
}

fun main() {
    html outer@ {
        this.body()
        [email protected]()
    }
}

Here:

this@outer

refers to the receiver of the lambda labeled outer.


4. super: call superclass implementation

Use super to access a member from the immediate superclass.

open class Parent {
    open fun greet() {
        println("Hello from Parent")
    }
}

class Child : Parent() {
    override fun greet() {
        super.greet()
        println("Hello from Child")
    }
}

Output:

Hello from Parent
Hello from Child

5. super<Type>: disambiguate multiple inherited implementations

If a class inherits the same member from multiple supertypes, you must specify which one to call.

interface A {
    fun greet() {
        println("Hello from A")
    }
}

interface B {
    fun greet() {
        println("Hello from B")
    }
}

class C : A, B {
    override fun greet() {
        super<A>.greet()
        super<B>.greet()
        println("Hello from C")
    }
}

Here:

super<A>.greet()
super<B>.greet()

select the specific supertype implementation.


6. super@Label: access an outer class’s superclass

In inner classes, super normally refers to the superclass of the inner class. If you need the superclass of an outer class, use a qualified super.

open class Base {
    open fun message() {
        println("Base")
    }
}

open class OuterBase : Base() {
    override fun message() {
        println("OuterBase")
    }
}

class Outer : OuterBase() {
    override fun message() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSuper() {
            [email protected]()
        }
    }
}

Here:

[email protected]()

means “call the superclass implementation of Outer”.

So this calls:

OuterBase.message()

not Outer.message().


7. Combining super<Type>@Label

If the outer class implements multiple supertypes, you can combine both forms.

interface A {
    fun print() {
        println("A")
    }
}

interface B {
    fun print() {
        println("B")
    }
}

class Outer : A, B {
    override fun print() {
        println("Outer")
    }

    inner class Inner {
        fun callOuterSupers() {
            super<A>@Outer.print()
            super<B>@Outer.print()
        }
    }
}

Here:

super<A>@Outer.print()
super<B>@Outer.print()

means:

  • call A’s implementation as inherited by Outer
  • call B’s implementation as inherited by Outer

Summary

Syntax Meaning
this Current receiver
this@Outer this of a specific labeled or outer receiver
super Immediate superclass implementation
super<Type> Specific superclass or interface implementation
super@Outer Superclass implementation of an outer class
super<Type>@Outer Specific supertype implementation of an outer class

In short:

this@Something

chooses which object/receiver you mean.

super<Something>

chooses which superclass/interface implementation you mean.

super<Something>@Outer

chooses which supertype implementation of which outer receiver you mean.

How do I use delegation by `by` keyword to implement interfaces or properties in Kotlin?

In Kotlin, the by keyword is used for delegation. It lets one object delegate behavior to another object instead of implementing everything manually.

There are two common forms:

  1. Interface/class delegation
  2. Property delegation

1. Interface delegation

If a class implements an interface, it can delegate the implementation of that interface to another object using by.

Example

interface Printer {
    fun print(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println(message)
    }
}

class LoggingPrinter(
    private val printer: Printer
) : Printer by printer

Here, LoggingPrinter implements Printer, but the actual implementation is delegated to printer.

Usage:

fun main() {
    val printer = LoggingPrinter(ConsolePrinter())
    printer.print("Hello Kotlin")
}

Output:

Hello Kotlin

LoggingPrinter does not need to manually implement print() because Printer by printer forwards calls to the given printer.


Overriding delegated behavior

You can still override specific methods if you want custom behavior.

interface Printer {
    fun print(message: String)
}

class ConsolePrinter : Printer {
    override fun print(message: String) {
        println(message)
    }
}

class LoggingPrinter(
    private val printer: Printer
) : Printer by printer {
    override fun print(message: String) {
        println("Logging message: $message")
        printer.print(message)
    }
}

Now calls to print() use the overridden method in LoggingPrinter.

fun main() {
    val printer = LoggingPrinter(ConsolePrinter())
    printer.print("Hello Kotlin")
}

Output:

Logging message: Hello Kotlin
Hello Kotlin

2. Delegating multiple interfaces

A class can delegate multiple interfaces to different objects.

interface Reader {
    fun read(): String
}

interface Writer {
    fun write(value: String)
}

class FileReader : Reader {
    override fun read(): String = "file contents"
}

class ConsoleWriter : Writer {
    override fun write(value: String) {
        println(value)
    }
}

class FileProcessor(
    reader: Reader,
    writer: Writer
) : Reader by reader, Writer by writer

Usage:

fun main() {
    val processor = FileProcessor(FileReader(), ConsoleWriter())

    val text = processor.read()
    processor.write(text)
}

Important note about delegated members

If a delegated object calls one of its own methods internally, it does not dispatch to overrides in the delegating class.

interface Service {
    fun operation()
    fun run()
}

class DefaultService : Service {
    override fun operation() {
        println("Default operation")
    }

    override fun run() {
        operation()
    }
}

class CustomService(
    private val service: Service
) : Service by service {
    override fun operation() {
        println("Custom operation")
    }
}

Usage:

fun main() {
    val service = CustomService(DefaultService())
    service.operation()
    service.run()
}

Output:

Custom operation
Default operation

service.run() is delegated to DefaultService.run(), and inside that object, operation() resolves to DefaultService.operation().


Property delegation

Property delegation lets another object provide the getter and/or setter for a property.

The syntax is:

val propertyName: Type by delegate
var propertyName: Type by delegate

The delegate object must provide:

  • getValue(...) for val
  • getValue(...) and setValue(...) for var

1. Built-in property delegates

Kotlin provides several common delegates.


lazy

lazy initializes a value only when it is first accessed.

val expensiveValue: String by lazy {
    println("Computing value")
    "Hello"
}

fun main() {
    println("Before access")
    println(expensiveValue)
    println(expensiveValue)
}

Output:

Before access
Computing value
Hello
Hello

The initializer runs only once.


observable

Delegates.observable runs code whenever a property changes.

import kotlin.properties.Delegates

var name: String by Delegates.observable("Unknown") { property, oldValue, newValue ->
    println("${property.name} changed from $oldValue to $newValue")
}

fun main() {
    name = "Alice"
    name = "Bob"
}

Output:

name changed from Unknown to Alice
name changed from Alice to Bob

vetoable

Delegates.vetoable can reject a new value.

import kotlin.properties.Delegates

var age: Int by Delegates.vetoable(0) { _, _, newValue ->
    newValue >= 0
}

fun main() {
    age = 25
    println(age)

    age = -5
    println(age)
}

Output:

25
25

The assignment to -5 is rejected.


notNull

Delegates.notNull() is useful for non-null properties initialized later.

import kotlin.properties.Delegates

var username: String by Delegates.notNull()

fun main() {
    username = "alice"
    println(username)
}

If you read username before assigning it, Kotlin throws an exception.


Custom property delegate

You can create your own delegate by implementing getValue and optionally setValue.

import kotlin.reflect.KProperty

class LoggingDelegate {
    private var value: String = ""

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("Reading ${property.name}")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        println("Writing ${property.name}: $newValue")
        value = newValue
    }
}

class User {
    var name: String by LoggingDelegate()
}

Usage:

fun main() {
    val user = User()

    user.name = "Alice"
    println(user.name)
}

Output:

Writing name: Alice
Reading name
Alice

How getValue and setValue work

For:

var name: String by LoggingDelegate()

Kotlin roughly translates property access like this:

val delegate = LoggingDelegate()

delegate.setValue(thisRef, property, "Alice")
val value = delegate.getValue(thisRef, property)

The signatures are:

operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)

For top-level properties, thisRef is null.


Delegating to another property

You can also delegate one property to another property reference.

class User {
    var name: String = "Alice"

    var displayName: String by this::name
}

fun main() {
    val user = User()

    println(user.displayName)

    user.displayName = "Bob"

    println(user.name)
}

Output:

Alice
Bob

displayName delegates its storage to name.


Delegating to a map

A common pattern is delegating properties to a Map.

class User(
    private val data: Map<String, Any?>
) {
    val name: String by data
    val age: Int by data
}

Usage:

fun main() {
    val user = User(
        mapOf(
            "name" to "Alice",
            "age" to 30
        )
    )

    println(user.name)
    println(user.age)
}

Output:

Alice
30

For mutable properties, use MutableMap:

class MutableUser(
    private val data: MutableMap<String, Any?>
) {
    var name: String by data
    var age: Int by data
}

Summary

Use case Syntax Meaning
Interface delegation class C(d: I) : I by d Forward interface calls to d
Lazy property val x by lazy { ... } Compute once on first access
Observable property var x by Delegates.observable(...) React to changes
Vetoable property var x by Delegates.vetoable(...) Accept or reject changes
Custom delegate var x by MyDelegate() Delegate getter/setter logic
Map-backed property val x: T by map Read value from map by property name
Property reference var x by this::other Delegate to another property

In short:

class MyClass(delegate: SomeInterface) : SomeInterface by delegate

is for interface delegation, while:

val value by lazy { ... }
var name by MyDelegate()

is for property delegation.

How do I write custom getters and setters in Kotlin properties?

In Kotlin, properties can have custom getters and setters by defining get() and/or set(value) directly under the property.

var propertyName: Type = initialValue
    get() {
        return field
    }
    set(value) {
        field = value
    }

field is the backing field automatically generated by Kotlin when needed.

Custom getter

class Person(
    val firstName: String,
    val lastName: String
) {
    val fullName: String
        get() = "$firstName $lastName"
}

Usage:

val person = Person("Ada", "Lovelace")
println(person.fullName) // Ada Lovelace

Here, fullName does not store a value. It is computed every time it is accessed.

Custom setter

class User {
    var age: Int = 0
        set(value) {
            field = if (value >= 0) value else 0
        }
}

Usage:

val user = User()
user.age = -5
println(user.age) // 0

The setter validates the assigned value before storing it.

Custom getter and setter together

class Temperature {
    var celsius: Double = 0.0
        get() = field
        set(value) {
            field = value.coerceAtLeast(-273.15)
        }

    var fahrenheit: Double
        get() = celsius * 9 / 5 + 32
        set(value) {
            celsius = (value - 32) * 5 / 9
        }
}

Usage:

val temperature = Temperature()

temperature.celsius = 100.0
println(temperature.fahrenheit) // 212.0

temperature.fahrenheit = 32.0
println(temperature.celsius) // 0.0

Important rules

  • val properties can only have a custom getter, not a setter.
  • var properties can have both a getter and a setter.
  • Use field inside accessors when you want to refer to the property’s backing field.
  • Do not write propertyName = value inside its own setter, because that recursively calls the setter.

For example, avoid this:

var name: String = ""
    set(value) {
        name = value // recursive setter call
    }

Use this instead:

var name: String = ""
    set(value) {
        field = value
    }

Example with validation

class Product {
    var price: Double = 0.0
        set(value) {
            require(value >= 0) { "Price cannot be negative" }
            field = value
        }
}
val product = Product()
product.price = 19.99
println(product.price)

// product.price = -1.0
// Throws IllegalArgumentException: Price cannot be negative

So the basic pattern is:

var myProperty: String = ""
    get() = field
    set(value) {
        field = value.trim()
    }

How do I use sealed classes for exhaustive type-safe hierarchies in Kotlin?

In Kotlin, sealed classes (and sealed interfaces) let you model a closed, type-safe hierarchy: a fixed set of known subtypes. This is especially useful for things like UI state, results, commands, events, and domain-specific alternatives.

Basic idea

A sealed class restricts which classes can inherit from it.

sealed class Result

data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
data object Loading : Result()

Now Result can only be one of the known subclasses: Success, Error, or Loading.

Exhaustive when

The main benefit is that Kotlin can check whether a when expression handles every possible subtype.

fun render(result: Result): String {
    return when (result) {
        is Success -> "Data: ${result.data}"
        is Error -> "Error: ${result.message}"
        Loading -> "Loading..."
    }
}

Because Result is sealed, the compiler knows all possible cases. You do not need an else branch if all cases are covered.

If you add another subtype:

data object Empty : Result()

Then this when becomes incomplete, and the compiler will require you to handle Empty.

Prefer data object for singleton states

For sealed hierarchies with singleton cases, use data object:

sealed class UiState {
    data object Loading : UiState()
    data object Empty : UiState()
    data class Success(val items: List<String>) : UiState()
    data class Error(val cause: Throwable) : UiState()
}

Usage:

fun message(state: UiState): String =
    when (state) {
        UiState.Loading -> "Loading"
        UiState.Empty -> "No items"
        is UiState.Success -> "Loaded ${state.items.size} items"
        is UiState.Error -> "Failed: ${state.cause.message}"
    }

Sealed classes vs enums

Use an enum class when every case is a simple constant:

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

Use a sealed class when cases may carry different data:

sealed class PaymentStatus {
    data object Pending : PaymentStatus()
    data class Paid(val receiptId: String) : PaymentStatus()
    data class Failed(val reason: String) : PaymentStatus()
}

Sealed interfaces

A sealed interface is useful when subclasses may also extend another class, or when you want multiple sealed abstractions.

sealed interface NetworkState

data object Offline : NetworkState
data object Connecting : NetworkState
data class Online(val bandwidthMbps: Int) : NetworkState

Usage:

fun describe(state: NetworkState): String =
    when (state) {
        Offline -> "Offline"
        Connecting -> "Connecting"
        is Online -> "Online at ${state.bandwidthMbps} Mbps"
    }

Nesting subclasses inside the sealed type

A common style is to define all cases inside the sealed class for readability:

sealed class AuthResult {
    data class Success(val userId: String) : AuthResult()
    data object InvalidCredentials : AuthResult()
    data object NetworkFailure : AuthResult()
}

Then use it like this:

fun handle(result: AuthResult): String =
    when (result) {
        is AuthResult.Success -> "Welcome ${result.userId}"
        AuthResult.InvalidCredentials -> "Invalid username or password"
        AuthResult.NetworkFailure -> "Please check your connection"
    }

Generic sealed result type

A common pattern is a generic result wrapper:

sealed class AppResult<out T> {
    data class Success<T>(val value: T) : AppResult<T>()
    data class Failure(val error: Throwable) : AppResult<Nothing>()
    data object Loading : AppResult<Nothing>()
}

Example usage:

fun display(result: AppResult<String>): String =
    when (result) {
        is AppResult.Success -> "Value: ${result.value}"
        is AppResult.Failure -> "Error: ${result.error.message}"
        AppResult.Loading -> "Loading..."
    }

The out T makes AppResult covariant, so AppResult<String> can be used where AppResult<Any> is expected.

Rules to remember

In modern Kotlin:

  • Direct subclasses of a sealed class/interface must be in the same package.
  • They must be declared in the same module.
  • Sealed subclasses can be top-level or nested.
  • A sealed class is abstract by default.
  • Sealed classes cannot be instantiated directly.

Example:

sealed class Command

data class CreateUser(val name: String) : Command()
data class DeleteUser(val id: Long) : Command()
data object Sync : Command()

Exhaustive when as an expression

For exhaustiveness checking, prefer using when as an expression:

val text = when (state) {
    UiState.Loading -> "Loading"
    UiState.Empty -> "Empty"
    is UiState.Success -> "Success"
    is UiState.Error -> "Error"
}

If you use when only as a statement, exhaustiveness checking may be less useful depending on context and Kotlin version/settings.

Practical example: UI state

sealed interface ProfileUiState {
    data object Loading : ProfileUiState
    data class Loaded(
        val name: String,
        val email: String
    ) : ProfileUiState
    data class Failed(val message: String) : ProfileUiState
}

fun renderProfile(state: ProfileUiState): String =
    when (state) {
        ProfileUiState.Loading -> "Loading profile..."
        is ProfileUiState.Loaded -> """
            Name: ${state.name}
            Email: ${state.email}
        """.trimIndent()
        is ProfileUiState.Failed -> "Could not load profile: ${state.message}"
    }

Best practices

  • Use sealed types to represent a closed set of alternatives.
  • Use data class for cases with data.
  • Use data object for singleton cases.
  • Avoid unnecessary else branches in when; let the compiler check exhaustiveness.
  • Keep sealed hierarchies small and meaningful.
  • Prefer sealed interfaces when you need more flexible inheritance.
  • Prefer enums for simple constant-only sets.

In short: sealed classes give you algebraic data type-style modeling in Kotlin, with compiler-checked exhaustive handling through when.

How do I use enums and associate data or behavior in Kotlin?

In Kotlin, enum class is used for a fixed set of constants. Enums can have:

  • simple constants
  • constructor parameters / associated data
  • properties
  • functions
  • overridden behavior per constant
  • companion object utilities
  • implemented interfaces

Basic enum

enum class Direction {
    NORTH,
    SOUTH,
    EAST,
    WEST
}

Usage:

val direction = Direction.NORTH

when (direction) {
    Direction.NORTH -> println("Going up")
    Direction.SOUTH -> println("Going down")
    Direction.EAST -> println("Going right")
    Direction.WEST -> println("Going left")
}

Kotlin when is exhaustive for enums if you cover all constants, so you often do not need an else.


Enum with associated data

Enums can have a constructor.

enum class HttpStatus(val code: Int, val reason: String) {
    OK(200, "OK"),
    CREATED(201, "Created"),
    BAD_REQUEST(400, "Bad Request"),
    NOT_FOUND(404, "Not Found"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error")
}

Usage:

val status = HttpStatus.NOT_FOUND

println(status.code)   // 404
println(status.reason) // Not Found

Important syntax rule: if an enum has members after the constants, the constant list must end with a semicolon.

enum class HttpStatus(val code: Int) {
    OK(200),
    NOT_FOUND(404);

    fun isSuccess(): Boolean = code in 200..299
}

Enum with shared behavior

You can define functions inside the enum class.

enum class Planet(val mass: Double, val radius: Double) {
    EARTH(5.972e24, 6.371e6),
    MARS(6.39e23, 3.389e6),
    JUPITER(1.898e27, 6.9911e7);

    fun surfaceGravity(): Double {
        val gravitationalConstant = 6.67430e-11
        return gravitationalConstant * mass / (radius * radius)
    }
}

Usage:

println(Planet.EARTH.surfaceGravity())

Enum constants with different behavior

Each enum constant can override functions.

enum class Operation {
    PLUS {
        override fun apply(a: Int, b: Int): Int = a + b
    },
    MINUS {
        override fun apply(a: Int, b: Int): Int = a - b
    },
    TIMES {
        override fun apply(a: Int, b: Int): Int = a * b
    },
    DIVIDE {
        override fun apply(a: Int, b: Int): Int = a / b
    };

    abstract fun apply(a: Int, b: Int): Int
}

Usage:

val result = Operation.TIMES.apply(6, 7)
println(result) // 42

This pattern is useful when the enum represents a strategy or command.


Enum implementing an interface

Enums can implement interfaces.

interface Printable {
    fun label(): String
}

enum class Priority : Printable {
    LOW {
        override fun label(): String = "Low priority"
    },
    MEDIUM {
        override fun label(): String = "Medium priority"
    },
    HIGH {
        override fun label(): String = "High priority"
    }
}

Usage:

val priority: Printable = Priority.HIGH
println(priority.label())

You can also combine constructor data with an interface:

interface HasCode {
    val code: Int
}

enum class ErrorType(
    override val code: Int,
    val message: String
) : HasCode {
    VALIDATION(100, "Validation failed"),
    AUTHENTICATION(200, "Authentication failed"),
    NOT_FOUND(300, "Resource not found")
}

Companion object lookup helpers

A common pattern is looking up enum values by associated data.

enum class HttpStatus(val code: Int) {
    OK(200),
    CREATED(201),
    BAD_REQUEST(400),
    NOT_FOUND(404);

    companion object {
        fun fromCode(code: Int): HttpStatus? {
            return entries.find { it.code == code }
        }
    }
}

Usage:

val status = HttpStatus.fromCode(404)
println(status) // NOT_FOUND

In modern Kotlin, prefer entries over values():

HttpStatus.entries

instead of:

HttpStatus.values()

Built-in enum properties and functions

Every enum constant has:

val name: String
val ordinal: Int

Example:

enum class Color {
    RED,
    GREEN,
    BLUE
}

println(Color.RED.name)    // RED
println(Color.RED.ordinal) // 0

You can parse by name:

val color = enumValueOf<Color>("RED")
println(color) // RED

Or safely:

val color = Color.entries.find { it.name == "RED" }

Enum with custom display names

Avoid relying on name for user-facing text. Use a property instead.

enum class UserRole(val displayName: String) {
    ADMIN("Administrator"),
    EDITOR("Editor"),
    VIEWER("Viewer")
}

Usage:

println(UserRole.ADMIN.displayName) // Administrator

Enum with properties and computed values

enum class FileType(val extension: String) {
    TEXT("txt"),
    JSON("json"),
    CSV("csv");

    val mimeType: String
        get() = when (this) {
            TEXT -> "text/plain"
            JSON -> "application/json"
            CSV -> "text/csv"
        }
}

Usage:

println(FileType.JSON.extension) // json
println(FileType.JSON.mimeType)  // application/json

When to use enums

Use an enum when:

  • the set of values is fixed
  • each value is a singleton
  • you need exhaustive when handling
  • the values are known at compile time

Good examples:

enum class LogLevel {
    TRACE,
    DEBUG,
    INFO,
    WARN,
    ERROR
}
enum class PaymentStatus {
    PENDING,
    PAID,
    FAILED,
    REFUNDED
}

When not to use enums

If each variant needs different state shapes, consider a sealed class or sealed interface.

For example, this is better as a sealed type:

sealed interface UiState {
    data object Loading : UiState
    data class Success(val data: String) : UiState
    data class Error(val message: String) : UiState
}

Because Success and Error need per-instance data, not fixed singleton enum constants.


Quick summary

enum class Status(val code: Int) {
    ACTIVE(1),
    DISABLED(2),
    DELETED(3);

    fun isVisible(): Boolean = this != DELETED

    companion object {
        fun fromCode(code: Int): Status? =
            entries.find { it.code == code }
    }
}

Usage:

val status = Status.fromCode(1)

if (status?.isVisible() == true) {
    println("Show item")
}

Kotlin enums are best for fixed named values, and they can carry data and behavior just like small classes.

How do I use object expressions and object declarations in Kotlin?

Object expressions vs. object declarations in Kotlin

Kotlin has two closely related features:

  • Object expressions: create an anonymous object immediately.
  • Object declarations: create a named singleton object.

They both use the object keyword, but they are used for different purposes.


1. Object expressions

Use an object expression when you need a one-off object, often to implement an interface or extend a class without creating a named class.

Basic anonymous object

fun main() {
    val user = object {
        val name = "Ava"
        val age = 30

        fun greet() {
            println("Hello, my name is $name")
        }
    }

    println(user.name)
    user.greet()
}

Here, user is an anonymous object with properties and functions.


2. Implementing an interface with an object expression

Object expressions are commonly used for callbacks, listeners, and small implementations.

interface ClickListener {
    fun onClick()
}

fun setClickListener(listener: ClickListener) {
    listener.onClick()
}

fun main() {
    setClickListener(object : ClickListener {
        override fun onClick() {
            println("Button clicked")
        }
    })
}

The syntax is:

object : SomeInterface {
    override fun someFunction() {
        // implementation
    }
}

3. Extending a class with an object expression

You can also create an anonymous subclass.

open class Animal(val name: String) {
    open fun speak() {
        println("$name makes a sound")
    }
}

fun main() {
    val dog = object : Animal("Buddy") {
        override fun speak() {
            println("$name barks")
        }
    }

    dog.speak()
}

4. Implementing multiple types

An object expression can extend one class and implement one or more interfaces.

open class Logger {
    open fun log(message: String) {
        println("Log: $message")
    }
}

interface Closeable {
    fun close()
}

fun main() {
    val resource = object : Logger(), Closeable {
        override fun log(message: String) {
            println("Custom log: $message")
        }

        override fun close() {
            println("Resource closed")
        }
    }

    resource.log("Started")
    resource.close()
}

If a superclass has a constructor, call it after the class name:

object : Logger()

5. Object declarations

Use an object declaration when you want a named singleton: exactly one instance, created lazily when first used.

object DatabaseConfig {
    val url = "jdbc:postgresql://localhost:5432/app"
    val username = "admin"

    fun connect() {
        println("Connecting to $url as $username")
    }
}

fun main() {
    DatabaseConfig.connect()
}

You do not instantiate it with DatabaseConfig().

Use it directly by name:

DatabaseConfig.connect()

6. Object declarations can implement interfaces

interface Analytics {
    fun track(event: String)
}

object ConsoleAnalytics : Analytics {
    override fun track(event: String) {
        println("Tracking event: $event")
    }
}

fun main() {
    ConsoleAnalytics.track("AppOpened")
}

This is useful for global services, registries, configuration, or strategy objects.


7. Object declarations can extend classes

open class AppLogger {
    open fun info(message: String) {
        println("INFO: $message")
    }
}

object Logger : AppLogger() {
    override fun info(message: String) {
        println("[App] $message")
    }
}

fun main() {
    Logger.info("Application started")
}

8. Companion objects

A companion object is an object declaration inside a class. It is commonly used for factory methods and static-like members.

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name.trim())
        }
    }
}

fun main() {
    val user = User.create("  Ava  ")
    println(user.name)
}

You call companion object members through the class name:

User.create("Ava")

9. Named companion objects

A companion object can have a name.

class App {
    companion object Config {
        const val VERSION = "1.0.0"

        fun printVersion() {
            println("Version: $VERSION")
        }
    }
}

fun main() {
    App.printVersion()
    App.Config.printVersion()
}

Both calls are valid:

App.printVersion()
App.Config.printVersion()

10. Object expression visibility detail

If an anonymous object is stored in a local variable, you can access its members:

fun main() {
    val person = object {
        val name = "Mira"
        fun sayHi() = println("Hi, I am $name")
    }

    println(person.name)
    person.sayHi()
}

But if an anonymous object is returned from a public function, its specific members are not visible unless the return type exposes them.

class Factory {
    fun createPublic(): Any = object {
        val name = "Hidden"
    }

    private fun createPrivate() = object {
        val name = "Visible inside class"
    }

    fun demo() {
        val item = createPrivate()
        println(item.name)
    }
}

In this example:

fun createPublic(): Any = object {
    val name = "Hidden"
}

The caller only sees Any, not name.


11. Quick comparison

Feature Object expression Object declaration
Purpose One-off anonymous object Named singleton
Has a name? Usually no Yes
Instantiated with constructor? No No
Created when? Immediately where expression runs Lazily on first access
Common use Callbacks, temporary implementations Singletons, global config, shared services
Can extend classes? Yes Yes
Can implement interfaces? Yes Yes

Simple rule of thumb

Use an object expression when you need an object right here, right now:

val listener = object : ClickListener {
    override fun onClick() {
        println("Clicked")
    }
}

Use an object declaration when you need one shared named instance:

object AppSettings {
    val theme = "dark"
}

How do I create and use companion objects for static-like behavior in Kotlin?

In Kotlin, a companion object is an object declared inside a class that can hold members callable on the class itself, giving you static-like behavior.

Kotlin does not have Java-style static members directly. Instead, you usually use companion object.

Basic example

class User(val name: String) {
    companion object {
        const val DEFAULT_NAME = "Guest"

        fun createDefault(): User {
            return User(DEFAULT_NAME)
        }
    }
}

You can access companion object members using the class name:

fun main() {
    println(User.DEFAULT_NAME)

    val user = User.createDefault()
    println(user.name)
}

Output:

Guest
Guest

Why it feels like static

This:

User.createDefault()

is similar to calling a static method in Java:

User.createDefault();

But internally, Kotlin’s companion object is an actual singleton object associated with the class.

Companion object properties

You can put properties inside a companion object:

class Counter {
    companion object {
        var count = 0

        fun increment() {
            count++
        }
    }
}

Usage:

fun main() {
    Counter.increment()
    Counter.increment()

    println(Counter.count)
}

Output:

2

Named companion objects

A companion object can have a name:

class Database {
    companion object Factory {
        fun connect(): Database {
            return Database()
        }
    }
}

You can still access members through the class name:

val db = Database.connect()

Or through the companion object name:

val db = Database.Factory.connect()

Factory method example

A common use case is creating factory methods:

class Person private constructor(
    val name: String,
    val age: Int
) {
    companion object {
        fun of(name: String, age: Int): Person {
            require(age >= 0) { "Age cannot be negative" }
            return Person(name, age)
        }
    }
}

Usage:

fun main() {
    val person = Person.of("Alice", 30)
    println(person.name)
}

Constants in companion objects

For compile-time constants, use const val:

class ApiConfig {
    companion object {
        const val BASE_URL = "https://api.example.com"
        const val TIMEOUT_SECONDS = 30
    }
}

Usage:

println(ApiConfig.BASE_URL)

const val can only be used with primitive types and String.

Java interoperability

From Java, companion object members are normally accessed through Companion:

class MathUtils {
    companion object {
        fun double(x: Int): Int = x * 2
    }
}

Java usage:

int result = MathUtils.Companion.double(5);

If you want Java callers to use it like a real static method, add @JvmStatic:

class MathUtils {
    companion object {
        @JvmStatic
        fun double(x: Int): Int = x * 2
    }
}

Then Java can call:

int result = MathUtils.double(5);

For constants:

class Constants {
    companion object {
        const val APP_NAME = "MyApp"
    }
}

Java can access this as:

String appName = Constants.APP_NAME;

Companion objects can implement interfaces

Because companion objects are real objects, they can implement interfaces:

interface Parser<T> {
    fun parse(value: String): T
}

class User(val name: String) {
    companion object : Parser<User> {
        override fun parse(value: String): User {
            return User(value)
        }
    }
}

Usage:

fun main() {
    val user = User.parse("Alice")
    println(user.name)
}

You can also pass the companion object where the interface is expected:

fun <T> parseWith(parser: Parser<T>, value: String): T {
    return parser.parse(value)
}

val user = parseWith(User, "Bob")

Here, User refers to the companion object when used as a value.

Key points

  • Use companion object for static-like members.
  • Access members as ClassName.member.
  • Use const val for compile-time constants.
  • Use @JvmStatic if Java callers need static-style access.
  • Companion objects are real singleton objects.
  • Companion objects can have names and implement interfaces.

How do I use inner and nested classes in Kotlin?

In Kotlin, classes can be declared inside other classes in two main ways:

  1. Nested classes — default behavior
  2. Inner classes — declared with the inner keyword

Nested classes

A class declared inside another class is nested by default.

class Outer {
    class Nested {
        fun message(): String {
            return "Hello from Nested"
        }
    }
}

You create an instance of the nested class using the outer class name:

fun main() {
    val nested = Outer.Nested()
    println(nested.message())
}

Important point

A nested class does not have access to members of the outer class.

class Outer {
    private val name = "Outer"

    class Nested {
        fun printName() {
            // println(name) // Error: cannot access outer class member
        }
    }
}

This is similar to a static nested class in Java.


Inner classes

If you want the nested class to access members of the outer class, mark it with inner.

class Outer {
    private val name = "Outer"

    inner class Inner {
        fun message(): String {
            return "Hello from $name"
        }
    }
}

You create an inner class instance from an instance of the outer class:

fun main() {
    val outer = Outer()
    val inner = outer.Inner()

    println(inner.message())
}

Output:

Hello from Outer

Difference between nested and inner classes

Feature Nested class Inner class
Keyword No keyword needed Uses inner
Has reference to outer class No Yes
Can access outer members No Yes
Instantiation Outer.Nested() Outer().Inner()
Similar to Java static nested class non-static inner class

Accessing this from an inner class

Inside an inner class, this refers to the inner class instance.

To refer to the outer class instance, use this@Outer.

class Outer {
    private val value = "Outer value"

    inner class Inner {
        private val value = "Inner value"

        fun printValues() {
            println(value)
            println(this.value)
            println([email protected])
        }
    }
}

Output:

Inner value
Inner value
Outer value

Example with state

class ShoppingCart {
    private val items = mutableListOf<String>()

    fun addItem(item: String) {
        items.add(item)
    }

    inner class Summary {
        fun printSummary() {
            println("Cart has ${items.size} items")
            println(items.joinToString())
        }
    }
}

fun main() {
    val cart = ShoppingCart()
    cart.addItem("Book")
    cart.addItem("Pen")

    val summary = cart.Summary()
    summary.printSummary()
}

Output:

Cart has 2 items
Book, Pen

Here, Summary is an inner class because it needs access to items from ShoppingCart.


When to use each

Use a nested class when:

  • The class is logically grouped inside another class
  • It does not need access to the outer class instance
  • You want a namespace-like structure
class ApiResponse {
    class Error(val code: Int, val message: String)
}

Use an inner class when:

  • The class needs access to the outer class’s properties or functions
  • Each inner class instance is tied to a specific outer class instance
class Form {
    private val fields = mutableListOf<String>()

    inner class Validator {
        fun validate(): Boolean {
            return fields.isNotEmpty()
        }
    }
}

Summary

class Outer {
    class Nested {
        // No access to Outer instance
    }

    inner class Inner {
        // Has access to Outer instance
    }
}

Use nested classes by default, and use inner only when the inner class needs to access the outer class instance.

How do I use abstract classes and methods in Kotlin?

In Kotlin, an abstract class is a class that cannot be instantiated directly. It is meant to be subclassed.

An abstract method is a method declared without an implementation. Subclasses must override it.

Basic example

abstract class Animal {
    abstract fun makeSound()

    fun sleep() {
        println("Sleeping...")
    }
}

class Dog : Animal() {
    override fun makeSound() {
        println("Woof!")
    }
}

fun main() {
    val dog = Dog()
    dog.makeSound()
    dog.sleep()
}

Output:

Woof!
Sleeping...

Key points

1. Use abstract before the class

abstract class Shape

You cannot create an instance of it:

val shape = Shape() // Error

2. Abstract methods have no body

abstract fun area(): Double

A subclass must implement them:

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

3. Abstract classes can have regular methods

abstract class Shape {
    abstract fun area(): Double

    fun describe() {
        println("This is a shape")
    }
}

4. Abstract properties are allowed

abstract class Vehicle {
    abstract val maxSpeed: Int
}

class Car : Vehicle() {
    override val maxSpeed: Int = 200
}

5. Abstract classes can have constructors

abstract class Person(val name: String) {
    abstract fun work()
}

class Developer(name: String) : Person(name) {
    override fun work() {
        println("$name writes code")
    }
}

Complete example

abstract class Shape(val name: String) {
    abstract fun area(): Double

    fun printInfo() {
        println("$name has area ${area()}")
    }
}

class Rectangle(
    name: String,
    val width: Double,
    val height: Double
) : Shape(name) {
    override fun area(): Double {
        return width * height
    }
}

class Circle(
    name: String,
    val radius: Double
) : Shape(name) {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

fun main() {
    val shapes = listOf(
        Rectangle("Rectangle", 5.0, 3.0),
        Circle("Circle", 2.0)
    )

    for (shape in shapes) {
        shape.printInfo()
    }
}

Abstract class vs interface

Use an abstract class when you want to share state or constructor logic:

abstract class BaseRepository(val tableName: String) {
    abstract fun findAll(): List<String>
}

Use an interface when you mainly want to define behavior:

interface Drawable {
    fun draw()
}

A class can extend only one abstract class, but it can implement multiple interfaces:

abstract class Animal

interface Runnable {
    fun run()
}

interface Swimmable {
    fun swim()
}

class Duck : Animal(), Runnable, Swimmable {
    override fun run() {
        println("Duck runs")
    }

    override fun swim() {
        println("Duck swims")
    }
}

In short: use abstract class for a shared base with common implementation/state, and use abstract fun or abstract val for members subclasses must provide.