Author profile picture

When (and when not) to Use Type Parameter Constraints in Kotlin

Kotlin allows you to constrain a type parameter on a generic when you declare it, limiting the range of types that it can accept. Why is this helpful? Well, let’s take a look at an example.

Let’s say you’ve got a few pets at home, and you want to pick a favorite:

fun <T> chooseFavorite(pets: List<T>): T {
    val favorite = pets[random.nextInt(pets.size)]
    // This next line won't compile - because `name` can't be resolved
    println("My favorite pet is ${favorite.name}")
    return favorite
}

Blast! The println() statement won’t compile because you can’t get the value of name on T - because T could be anything that the caller specifies. For example, if you wrote the following, then T would be an Int, which doesn’t have a name property:

chooseFavorite(listOf(1, 2, 3))

Option 1 - Give up on Generics

Well, you could choose to change this function so that it’s no longer a generic:

fun chooseFavorite(pets: List<Pet>): Pet {
    val favorite = pets[random.nextInt(pets.size)]
    println("My favorite pet is ${favorite.name}")
    return favorite
}

That would work, but then we’re stuck with a return type that we might not want. Here’s what happens when we call that function:

val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
val favorite: Pet = chooseFavorite(pets)

Even though we start with a List<Dog>, when we get the favorite back, it’s a Pet. If we want a reference to Rover or Sheba as a Dog, we would have to cast it back. Yikes!

Option 2 - Use Type Parameter Constraints

We can limit the type parameter by specifying an upper bound - in other words, the super-most type that you want to accept. In our case, we want this function to work on a List<Dog>, a List<Cat>, or a mixed list of both cats and dogs, as List<Pet>. Easily extended to ferrets and iguanas if that’s your jam.

fun <T : Pet> chooseFavorite(pets: List<T>): T {
    val favorite = pets[random.nextInt(pets.size)]
    println("My favorite pet is ${favorite.name}")
    return favorite
}

The type parameter declaration here is <T : Pet>, where Pet is the upper bound. Now that we’ve specified this, the calling code can only pass a List of Pet and subtypes of Pet. Aaaaand… the return type is exactly what we want:

val pets: List<Dog> = listOf(Dog("Rover"), Dog("Sheba"))
val favorite: Dog = chooseFavorite(pets)

Recommendation

You’ll want to use a type parameter constraint when the following characteristics line up:

  1. You want to invoke a particular function or property on the type.
  2. You want to preserve the type when it’s returned.

Here’s a quick “cheat sheet” style table to help you decide what to use.

  Need to invoke member No need to invoke member
Need to preserve the type Use a generic with type parameter constraint Use a generic without type parameter constraint
No need to preserve the type Use a non-generic with proper abstraction Go use a raw type in Java… ;)

How to Specify Constraints

There’s a lot more to constraints - you can specify constraints on multiple parameters, and multiple constraints on the same parameter. For all the nitty gritty, go check out the concept article, Type Parameter Constraint. You can also find a quick overview of generic constraints in Kotlin’s official reference documentation.

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit