Author profile picture

Type Parameter Constraint

Overview

Type parameter constraints allow you to limit the types that can be accepted as a type argument to an upper bound, giving you the ability to invoke functions and properties on an object while preserving the type.

If that sounds confusing, go check out the guide, When to Use Type Parameter Constraints, which walks through the use cases in more detail.

In this article, we’re going to look at the variety of ways that you can create a type parameter constraint in Kotlin. Type parameter declarations can go on interfaces, classes, functions, and extension properties, but since functions demonstrate things so well, we’ll use them for our examples.

Examples: The Concise Way

Single Type Parameter

The easiest way to constrain a type parameter is to name a type in the type parameter declaration. For example, we’ll put : Pet inside our declaration here:

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

In this case, the code that calls chooseFavorite() can supply a List of anything that’s of type Pet, but it couldn’t, for example, supply a List<String> or List<Int>:

val cats: List<Cat> = listOf(Cat("Whiskers"), Cat("Rosie"))
val favorite: Cat = chooseFavorite(cats)

Multiple Type Parameters

You can specify multiple type parameters by separating them with a comma, and each one can have a constraint:

fun <T : Pet, U : Human> chooseFavorite(pets: List<T>, masters: List<U>): T {
    val master = masters[random.nextInt(masters.size)]
    val favorite = pets[random.nextInt(pets.size)]

    println("${favorite.name} is ${master.name}'s favorite")
    return favorite
}

In this example, the chooseFavorite() function declares two type parameters - T and U, which means:

  • pets can be a List of anything Pet or subtype of Pet (as above), and…
  • masters can be a List of anything Human or subtype of Human

Examples: The Verbose Way

Single Type Parameters

Alternatively, you can put the constraint after the return type, following the keyword where. Let’s take our first example and rework it to this more verbose syntax.

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

The where T : Pet indicates the same thing as <T : Pet> did in the first example.

Multiple Type Parameters

As you’d expect, you also can specify multiple type parameter constraints using the where syntax:

fun <T, U> chooseFavorite(pets: List<T>, masters: List<U>): T where T : Pet, U : Human {
    val master = masters[random.nextInt(masters.size)]
    val favorite = pets[random.nextInt(pets.size)]
    println("${favorite.name} is ${master.name}'s favorite")
    return favorite
}

Well, if you can achieve the same thing with both a concise syntax and a more verbose syntax, why would anyone choose the more verbose?

Multiple Constraints on One Parameter

The answer, as you probably guessed, is that it lets you do more: it lets you specify multiple constraints on a single parameter.

So, imagine that we don’t have a Pet type, but instead we have an Animal interface (that has a species property) and a Named interface (that has a name property). In that case, any class that implements both of these interfaces can fit the definition of a pet. Here’s how you’d write the constraints.

fun <T> chooseFavorite(pets: List<T>): T where T : Animal, T : Named {
    val favorite = pets[random.nextInt(pets.size)]
    println("${favorite.name} is the favorite ${favorite.species}")
    return favorite
}

(Fascinating side note: the compiled bytecode effectively uses the first type constraint as the return type - in this case, Animal. The return type doesn’t really matter, though, because it’ll add casts everywhere that it’s needed.)

Putting Constraints in Both Places

Ready to live life on the edge?! Hold onto your cargo pockets - we’re gonna get dangerous here…

We’re going to split our constraints! Let’s put Animal within the angle brackets and Named in the where clause:

fun <T : Animal> chooseFavorite(pets: List<T>): T where T : Named { /* ... */ }

This fails inspection in the IDE with this message:

“If a type parameter has multiple constraints, they all need to be placed in the ‘where’ clause” (Source)

However, this still compiled and executed for me. So, you technically can split multiple constraints for the same parameter by putting the first within the <T> declaration and the rest in the where clause.

But c’mon, now… you’re a better programmer than that…

Default Constraint

If you don’t specify a constraint, the default constraint will be Any?.

fun <T> chooseFavorite(things: List<T>): T { /* ... */ }

is the exactly the same as

fun <T : Any?> chooseFavorite(things: List<T>): T { /* ... */ }

Nulls

If you want to specify a nullable type for the constraint, it’ll work as expected:

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

val maybeCats: List<Cat?> = listOf(Cat("Whiskers"), null, Cat("Rosie"))
val favorite: Cat? = chooseFavorite(maybeCats)

Just remember that the ? goes on the type constraint, not the type parameter references. In other words, in this example, you wouldn’t specify List<T?> or return T?.

Any Non-Null

Naturally, if you want to accept any type that isn’t null, you’d just use Any as the constraint:

fun <T : Any> chooseFavorite(things: List<T>): T { /* ... */ }

Recommendations

I’ve not seen much talk in the Kotlin community about best practices for type parameter constraints, but here are some common-sense guidelines.

  • Favor the concise syntax unless you need to specify multiple constraints on a single type parameter.
  • When using a where clause, put all of the constraints there.
  • Don’t specify a constraint of Any? since that’s the default.

Check out the guide, When to Use Type Parameter Constraints, for additional examples and explanations.

Share this article: