ZetCode

Kotlin where Keyword

last modified April 19, 2025

Kotlin's generic type system allows constraining type parameters with multiple requirements. The where keyword specifies these constraints. This tutorial explores the where keyword in depth with practical examples.

Basic Definitions

The where keyword in Kotlin applies multiple constraints to generic type parameters. It's used when a type parameter must satisfy several conditions. Constraints can include class/interface implementations and other type relations.

Single Constraint with where

The simplest use of where applies one constraint to a type parameter. This is equivalent to using a colon syntax but demonstrates the basic structure.

SingleConstraint.kt
package com.zetcode

interface Printable {
    fun print()
}

class Document : Printable {
    override fun print() = println("Printing document")
}

fun <T> printItem(item: T) where T : Printable {
    item.print()
}

fun main() {
    val doc = Document()
    printItem(doc) // Output: Printing document
}

Here we define a generic function printItem that requires its type parameter T to implement Printable. The where clause enforces this constraint. The Document class satisfies this requirement.

Multiple Constraints on Single Type

The where keyword shines when you need multiple constraints on one type parameter. This example requires a type to implement two interfaces.

MultipleConstraints.kt
package com.zetcode

interface Serializable {
    fun serialize(): String
}

interface Deserializable {
    fun deserialize(data: String)
}

class Config : Serializable, Deserializable {
    override fun serialize() = "Config data"
    override fun deserialize(data: String) = println("Loading: $data")
}

fun <T> processData(item: T) where T : Serializable, T : Deserializable {
    val data = item.serialize()
    item.deserialize(data)
}

fun main() {
    val config = Config()
    processData(config) // Output: Loading: Config data
}

The processData function requires its type parameter T to implement both Serializable and Deserializable. The Config class meets these requirements, so we can call the function with a Config instance.

Constraints on Multiple Type Parameters

where can constrain multiple type parameters in a single declaration. This is useful when types need to relate to each other in specific ways.

MultiTypeConstraints.kt
package com.zetcode

interface Producer<out T> {
    fun produce(): T
}

interface Consumer<in T> {
    fun consume(item: T)
}

fun <T, U> transform(
    producer: Producer<T>,
    consumer: Consumer<U>
) where T : U, U : Number {
    val item = producer.produce()
    consumer.consume(item)
}

class IntProducer : Producer<Int> {
    override fun produce() = 42
}

class NumberConsumer : Consumer<Number> {
    override fun consume(item: Number) = println("Consumed: $item")
}

fun main() {
    transform(IntProducer(), NumberConsumer()) // Output: Consumed: 42
}

This example shows constraints on two type parameters T and U. T must be a subtype of U, and U must be a subtype of Number. The transform function can only be called with types satisfying these relationships.

Class with where Constraints

The where keyword can also be used with class declarations to constrain their type parameters. This ensures all class methods have access to the constrained types.

ClassConstraints.kt
package com.zetcode

interface Identifiable {
    val id: String
}

interface Timestamped {
    val timestamp: Long
}

class Repository<T>(private val items: List<T>) where T : Identifiable, T : Timestamped {
    fun findById(id: String): T? = items.find { it.id == id }
    
    fun getRecent(): List<T> {
        val now = System.currentTimeMillis()
        return items.filter { now - it.timestamp < 3600000 }
    }
}

data class LogEntry(
    override val id: String,
    override val timestamp: Long,
    val message: String
) : Identifiable, Timestamped

fun main() {
    val logs = listOf(
        LogEntry("1", System.currentTimeMillis() - 1000, "Started"),
        LogEntry("2", System.currentTimeMillis() - 7200000, "Old entry")
    )
    
    val repo = Repository(logs)
    println(repo.findById("1")?.message) // Output: Started
    println(repo.getRecent().size)       // Output: 1
}

The Repository class requires its type parameter T to implement both Identifiable and Timestamped. This allows the class methods to safely access id and timestamp properties. LogEntry satisfies these constraints.

Combining Class and Function Constraints

When both class and function have where constraints, they combine to create even stricter type requirements. This provides fine-grained control over generic types.

CombinedConstraints.kt
package com.zetcode

interface Named {
    val name: String
}

interface Priced {
    val price: Double
}

class Store<T> where T : Named {
    private val items = mutableListOf<T>()
    
    fun addItem(item: T) = items.add(item)
    
    fun <U> findCheaperThan(maxPrice: Double): List<U> 
        where U : T, U : Priced {
        return items.filterIsInstance<U>().filter { it.price <= maxPrice }
    }
}

data class Product(
    override val name: String,
    override val price: Double
) : Named, Priced

fun main() {
    val store = Store<Named>()
    store.addItem(Product("Laptop", 999.99))
    store.addItem(Product("Mouse", 25.50))
    
    val affordable = store.findCheaperThan<Product>(100.0)
    println(affordable.map { it.name }) // Output: [Mouse]
}

The Store class constrains T to Named, while its findCheaperThan method further requires U to be both a subtype of T and implement Priced. This ensures we can access both name and price properties in the filtered results.

Recursive Type Constraints

where clauses can express recursive type constraints, where a type parameter must relate to itself in specific ways. This is useful for comparison operations.

RecursiveConstraints.kt
package com.zetcode

interface Comparable<in T> {
    fun compareTo(other: T): Int
}

fun <T> max(a: T, b: T): T where T : Comparable<T> {
    return if (a.compareTo(b) >= 0) a else b
}

data class Version(val major: Int, val minor: Int) : Comparable<Version> {
    override fun compareTo(other: Version): Int {
        return when {
            major != other.major -> major - other.major
            else -> minor - other.minor
        }
    }
}

fun main() {
    val v1 = Version(2, 5)
    val v2 = Version(2, 7)
    println(max(v1, v2)) // Output: Version(major=2, minor=7)
}

The max function requires T to implement Comparable<T>, meaning instances of T can be compared to other instances of T. Version satisfies this constraint by implementing compareTo for Version objects.

Complex Multiple Type Relationships

The most powerful use of where establishes complex relationships between multiple type parameters. This example shows a data processor with multiple constraints.

ComplexRelationships.kt
package com.zetcode

interface Entity<ID> {
    val id: ID
}

interface Repository<E, ID> where E : Entity<ID>, ID : Comparable<ID> {
    fun save(entity: E): E
    fun findById(id: ID): E?
}

data class User(
    override val id: String,
    val name: String
) : Entity<String>

class UserRepository : Repository<User, String> {
    private val storage = mutableMapOf<String, User>()
    
    override fun save(entity: User): User {
        storage[entity.id] = entity
        return entity
    }
    
    override fun findById(id: String) = storage[id]
}

fun main() {
    val repo = UserRepository()
    val user = repo.save(User("123", "Alice"))
    println(repo.findById("123")?.name) // Output: Alice
}

This example establishes that E must be an Entity<ID> and ID must be Comparable<ID>. The UserRepository implements this interface with String as ID and User as E. String is Comparable, and User implements Entity<String>.

Best Practices for where Clauses

Source

Kotlin Generics Documentation

This tutorial covered Kotlin's where keyword in depth, showing how to apply multiple constraints to generic type parameters. We explored various scenarios from simple to complex type relationships. Proper use of constraints can make your generic code more type-safe while maintaining flexibility.

Author

My name is Jan Bodnar, and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Kotlin tutorials.