Skip to main content

Kotlin Type Parameters

Type parameters are a fundamental concept in Kotlin's generics system that allows you to write flexible and reusable code while maintaining type safety. If you've ever wanted to create functions or classes that can work with different types without sacrificing compile-time type checking, type parameters are your answer.

What are Type Parameters?

Type parameters allow you to define classes, interfaces, and functions that can operate on different types without specifying the exact type in advance. Instead of committing to a specific type, you use a placeholder (the type parameter) that will be replaced with an actual type when the code is used.

In Kotlin, type parameters are defined within angle brackets <> and are typically named using single uppercase letters like T, E, K, or V, though you can use more descriptive names for clarity.

Basic Syntax of Type Parameters

Let's start with a simple example of a function using a type parameter:

kotlin
fun <T> printItem(item: T) {
println("Item: $item")
}

// Usage
fun main() {
printItem("Hello") // Works with String
printItem(42) // Works with Int
printItem(3.14) // Works with Double
printItem(true) // Works with Boolean
}

Output:

Item: Hello
Item: 42
Item: 3.14
Item: true

In this example, T is a type parameter that can represent any type. The function printItem can accept an argument of any type, making it very flexible.

Creating Generic Classes

Type parameters are commonly used to create generic classes that can work with different types:

kotlin
class Box<T>(val value: T) {
fun getValue(): T {
return value
}
}

fun main() {
val stringBox = Box("Hello Kotlin")
val intBox = Box(123)

println("String box value: ${stringBox.getValue()}")
println("Integer box value: ${intBox.getValue()}")

// The compiler ensures type safety
val stringValue: String = stringBox.getValue() // OK
val intValue: Int = intBox.getValue() // OK

// This would cause a compilation error:
// val wrongType: Double = intBox.getValue()
}

Output:

String box value: Hello Kotlin
Integer box value: 123

Here, Box<T> is a generic class that can hold any type of value. When you create an instance of Box, you specify the actual type, and Kotlin ensures type safety throughout your code.

Multiple Type Parameters

You can define classes and functions with multiple type parameters:

kotlin
class Pair<A, B>(val first: A, val second: B) {
override fun toString(): String = "($first, $second)"
}

fun <K, V> createMap(key: K, value: V): Map<K, V> {
return mapOf(key to value)
}

fun main() {
val pair = Pair("name", 30)
println("Pair: $pair")

val map = createMap("id", 12345)
println("Map: $map")
}

Output:

Pair: (name, 30)
Map: {id=12345}

This example shows how to use multiple type parameters in both a class (Pair<A, B>) and a function (createMap<K, V>).

Type Constraints

Sometimes you want to restrict the types that can be used with your generic code. In Kotlin, you can use type constraints to specify that a type parameter must be a subtype of a certain type:

kotlin
fun <T : Comparable<T>> findMax(a: T, b: T): T {
return if (a > b) a else b
}

fun main() {
println("Max of 5 and 10: ${findMax(5, 10)}")
println("Max of 'a' and 'z': ${findMax('a', 'z')}")
println("Max of \"apple\" and \"banana\": ${findMax("apple", "banana")}")

// This would cause a compilation error because Boolean doesn't implement Comparable:
// findMax(true, false)
}

Output:

Max of 5 and 10: 10
Max of 'a' and 'z': z
Max of "apple" and "banana": banana

In this example, the type parameter T is constrained to types that implement the Comparable<T> interface, which includes types like Int, Char, and String but excludes types like Boolean.

Type Parameter Naming Conventions

While single letters are commonly used for type parameters, you can use more descriptive names for clarity, especially in complex code:

kotlin
fun <Element> List<Element>.firstOrNull(): Element? {
return if (this.isEmpty()) null else this[0]
}

fun <InputType, OutputType> transform(input: InputType, transformer: (InputType) -> OutputType): OutputType {
return transformer(input)
}

Real-World Example: Repository Pattern

Here's a practical example showing how type parameters can be used to create a flexible repository interface for data access:

kotlin
// Define a generic repository interface
interface Repository<T, ID> {
fun findById(id: ID): T?
fun save(entity: T): T
fun delete(id: ID)
fun findAll(): List<T>
}

// A sample entity class
data class User(val id: Long, val name: String, val email: String)

// Implementation for User entities
class UserRepository : Repository<User, Long> {
private val users = mutableMapOf<Long, User>()

override fun findById(id: Long): User? {
return users[id]
}

override fun save(entity: User): User {
users[entity.id] = entity
return entity
}

override fun delete(id: Long) {
users.remove(id)
}

override fun findAll(): List<User> {
return users.values.toList()
}
}

fun main() {
val userRepo = UserRepository()

// Add some users
userRepo.save(User(1, "Alice", "[email protected]"))
userRepo.save(User(2, "Bob", "[email protected]"))

// Find all users
val allUsers = userRepo.findAll()
println("All users: $allUsers")

// Find user by ID
val user = userRepo.findById(1)
println("User with ID 1: $user")

// Delete user
userRepo.delete(2)
println("After deletion: ${userRepo.findAll()}")
}

Output:

All users: [User(id=1, name=Alice, [email protected]), User(id=2, name=Bob, [email protected])]
User with ID 1: User(id=1, name=Alice, [email protected])
After deletion: [User(id=1, name=Alice, [email protected])]

This example demonstrates how type parameters make our repository interface reusable across different entity types while maintaining type safety.

Type Parameters vs. Any Type

You might wonder why we use type parameters instead of just using Any (Kotlin's equivalent of Java's Object):

kotlin
// Using Any (not recommended)
class BoxWithAny(val value: Any) {
fun getValue(): Any = value
}

// Using type parameter (better approach)
class BoxWithTypeParam<T>(val value: T) {
fun getValue(): T = value
}

fun main() {
val anyBox = BoxWithAny("Hello")
// Need explicit casting, potential for runtime errors
val anyString = anyBox.getValue() as String

val genericBox = BoxWithTypeParam("World")
// No casting needed, compile-time type safety
val genericString: String = genericBox.getValue()

println("From Any box: $anyString")
println("From generic box: $genericString")
}

Output:

From Any box: Hello
From generic box: World

The key difference is that with type parameters:

  1. You get compile-time type checking
  2. You don't need explicit type casting
  3. You avoid potential ClassCastException at runtime

Summary

Kotlin type parameters provide a powerful mechanism for creating reusable and type-safe code:

  • They allow you to write generic classes, interfaces, and functions
  • You can constrain type parameters using the : SuperType syntax
  • Using type parameters maintains compile-time type safety
  • Multiple type parameters can be used simultaneously
  • They follow naming conventions (typically uppercase letters)
  • They're essential for implementing common design patterns like repositories

By mastering type parameters, you'll be able to write more flexible, reusable, and robust Kotlin code.

Practice Exercises

  1. Create a generic Stack<T> class with push(), pop(), and peek() methods
  2. Implement a swap<T> function that exchanges the values of two variables
  3. Create a Filterable<T> interface with a filter() method and implement it for different collections
  4. Design a generic Result<S, E> class that can represent either a success (with data of type S) or an error (with data of type E)

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)