Skip to main content

Kotlin Type Inference

Introduction

One of Kotlin's most powerful features is its ability to infer types without requiring explicit type declarations. Type inference allows you to write cleaner, more concise code while still maintaining all the benefits of a statically typed language. This feature makes Kotlin more approachable for beginners while giving experienced developers tools to write expressive code with less boilerplate.

In this tutorial, we'll explore how Kotlin's type inference works, when to rely on it, and when you might want to declare types explicitly.

What is Type Inference?

Type inference is the ability of a programming language to automatically detect the type of an expression without the developer having to specify it explicitly. In Kotlin, the compiler analyzes the context of your code and determines the most appropriate type for variables, functions, and expressions.

Let's compare Java (which has limited type inference) with Kotlin to see the difference:

In Java:

java
String greeting = "Hello, world!";
List<String> names = new ArrayList<String>();

In Kotlin:

kotlin
val greeting = "Hello, world!" // Type is inferred as String
val names = mutableListOf<String>() // Type is inferred as MutableList<String>

How Type Inference Works in Kotlin

Variable Type Inference

When you declare a variable in Kotlin using val or var, you can often omit the type declaration:

kotlin
val name = "John Doe" // Inferred as String
val age = 30 // Inferred as Int
val isStudent = false // Inferred as Boolean

Let's see the types being inferred at runtime:

kotlin
val name = "John Doe"
val age = 30
val isStudent = false

println("name is of type: ${name::class.simpleName}")
println("age is of type: ${age::class.simpleName}")
println("isStudent is of type: ${isStudent::class.simpleName}")

Output:

name is of type: String
age is of type: Int
isStudent is of type: Boolean

Function Return Type Inference

Kotlin can also infer the return type of functions:

kotlin
fun add(a: Int, b: Int) = a + b // Return type is inferred as Int

fun getMessage() = "Welcome to Kotlin" // Return type is inferred as String

Type Inference with Generic Functions

Kotlin's type inference works well with generics too:

kotlin
fun <T> List<T>.getSecondOrNull(): T? {
return if (this.size >= 2) this[1] else null
}

val numbers = listOf(1, 2, 3, 4)
val secondNumber = numbers.getSecondOrNull() // Inferred as Int?

val names = listOf("Alice", "Bob", "Charlie")
val secondName = names.getSecondOrNull() // Inferred as String?

println("secondNumber is of type: ${secondNumber?.let { it::class.simpleName } ?: "null"}")
println("secondName is of type: ${secondName?.let { it::class.simpleName } ?: "null"}")

Output:

secondNumber is of type: Int
secondName is of type: String

Smart Casts with Type Inference

Kotlin combines type inference with smart casts to make your code even more concise:

kotlin
fun printLength(obj: Any) {
if (obj is String) {
// Kotlin knows obj is String in this scope
println("The string length is ${obj.length}")
} else {
println("This is not a string")
}
}

printLength("Hello Kotlin")
printLength(123)

Output:

The string length is 11
This is not a string

When to Declare Types Explicitly

While type inference is powerful, there are situations where it's better to specify types explicitly:

1. For Public API Clarity

When designing public APIs, explicit types make your code more self-documenting:

kotlin
// Less clear for API users
fun process(data) = data.process() // What type is data?

// More clear and self-documenting
fun process(data: DataProcessor): ProcessResult = data.process()

2. When Inference Might Be Ambiguous

Sometimes the compiler needs your help to determine the correct type:

kotlin
// This is ambiguous - what type should the empty list be?
val emptyList = listOf() // Inferred as List<Nothing>

// Better to be explicit
val emptyStringList = listOf<String>() // Now clearly List<String>

3. To Enforce Specific Types

Sometimes you want to enforce a specific type for correctness or interoperability:

kotlin
// Without explicit type, this is inferred as Double
val distanceInKm = 5.0

// With explicit type, we enforce Int
val timeInMinutes: Int = 30

Real-World Applications

Building a Simple Generic Repository

Here's a practical example showing how type inference makes generic programming cleaner:

kotlin
interface Repository<T> {
fun save(item: T): T
fun findAll(): List<T>
}

class UserRepository : Repository<User> {
private val users = mutableListOf<User>()

override fun save(item: User): User {
users.add(item)
return item
}

override fun findAll(): List<User> = users.toList()
}

data class User(val id: Int, val name: String)

// Usage
fun main() {
val userRepo = UserRepository()

// Type inference works with our generic repository
userRepo.save(User(1, "Alice"))
userRepo.save(User(2, "Bob"))

val allUsers = userRepo.findAll()
// allUsers is inferred as List<User>

allUsers.forEach { println("User: ${it.name}") }
}

Building a DSL (Domain Specific Language)

Type inference is particularly powerful when building DSLs:

kotlin
class HTMLBuilder {
private val content = StringBuilder()

fun h1(text: String) {
content.append("<h1>$text</h1>")
}

fun p(text: String) {
content.append("<p>$text</p>")
}

fun build(): String = content.toString()
}

fun html(init: HTMLBuilder.() -> Unit): String {
val builder = HTMLBuilder()
builder.init()
return builder.build()
}

// Usage with type inference making the DSL clean
fun createPage() = html {
h1("Welcome to Kotlin")
p("Type inference makes DSLs elegant!")
}

fun main() {
val page = createPage()
println(page)
}

Output:

<h1>Welcome to Kotlin</h1><p>Type inference makes DSLs elegant!</p>

Summary

Kotlin's type inference is a powerful feature that helps you write cleaner, more concise code without sacrificing type safety. Here's what we've learned:

  • Type inference determines types automatically based on context
  • It works for variables, function return types, and generic functions
  • It combines with smart casts to make your code even more concise
  • There are situations where explicit type declarations are still valuable

By understanding when to rely on type inference and when to declare types explicitly, you'll write more maintainable Kotlin code that's both concise and clear.

Additional Resources

Exercises

  1. Create a function that takes a list of any type and returns the first and last elements as a Pair. Use type inference appropriately.
  2. Write a class that holds a value of any type, with methods to transform that value. Implement it so that type inference works correctly.
  3. Create a small DSL for building JSON objects, utilizing type inference to make the syntax clean and intuitive.


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