Scala Type Inference

Type inference is Scala's ability to figure out the type of a value automatically, without you declaring it explicitly. The compiler analyzes your code and determines what type each expression produces. This reduces boilerplate while keeping all the safety benefits of a statically typed language.

The Basic Idea

// Explicit type declaration
val age: Int = 25

// Inferred — Scala sees 25 and knows it's an Int
val age = 25

// Both are identical to the compiler

Think of type inference like autocomplete in a messaging app. You start typing a word, and the app figures out what you mean. Scala's compiler does the same for types — it reads your code and fills in the type information.

Inference for Variables

val name = "Scala"          // String
val count = 42              // Int
val price = 9.99            // Double
val active = true           // Boolean
val items = List(1, 2, 3)   // List[Int]
val pair = (10, "hello")    // (Int, String)

// Check types in the REPL
scala> val x = 100
val x: Int = 100

scala> val y = List("a", "b")
val y: List[String] = List(a, b)

Inference for Functions

Scala infers return types from the function body:

// Return type inferred as Int
def add(a: Int, b: Int) = a + b

// Return type inferred as String
def greet(name: String) = "Hello, " + name

// Return type inferred as Boolean
def isAdult(age: Int) = age >= 18

// Return type inferred as List[Int]
def doubleAll(nums: List[Int]) = nums.map(_ * 2)

Note: parameter types are NOT inferred. You must always declare them explicitly. The compiler cannot know what type a caller will pass.

Inference with Collections

val ints = List(1, 2, 3)           // List[Int]
val strs = List("a", "b", "c")    // List[String]
val mixed = List(1, "two", 3.0)   // List[Any]  ← usually undesirable
val nested = List(List(1, 2), List(3, 4))  // List[List[Int]]

val map = Map("one" -> 1, "two" -> 2)  // Map[String, Int]
val set = Set(1, 2, 3, 2, 1)           // Set[Int]

Inference Through Transformations

Scala tracks types through a chain of operations:

val numbers = List(1, 2, 3, 4, 5)       // List[Int]
val doubled = numbers.map(_ * 2)         // List[Int]
val strings = numbers.map(_.toString)    // List[String]
val filtered = numbers.filter(_ > 2)     // List[Int]
val sum = numbers.foldLeft(0)(_ + _)     // Int

When Inference Widens the Type

When you mix types in a collection, Scala infers the most specific common supertype:

class Animal
class Dog extends Animal
class Cat extends Animal

val animals = List(new Dog(), new Cat())   // List[Animal]

val nums = List(1, 2L, 3.0)   // List[Double] — widened to Double

  Dog     Cat
    \     /
    Animal   ← common supertype
    
List(Dog, Cat) → List[Animal]

Inference with Generic Functions

def identity[A](x: A): A = x

identity(42)        // A inferred as Int   → returns Int
identity("hello")   // A inferred as String → returns String
identity(List(1))   // A inferred as List[Int] → returns List[Int]

Limits of Inference

Type inference works most of the time, but some situations require explicit annotations:

1. Recursive Functions

// WRONG — compiler cannot infer return type of recursive functions
def factorial(n: Int) = if n <= 1 then 1 else n * factorial(n - 1)
// Error: recursive method needs result type

// CORRECT — add return type
def factorial(n: Int): Int = if n <= 1 then 1 else n * factorial(n - 1)

2. Overloaded Methods

// When inference is ambiguous, add type annotation
val result: Double = 10 / 3.0   // clearly Double

3. Empty Collections

// WRONG — Scala infers List[Nothing] for empty literals
val empty = List()   // List[Nothing] — nearly useless

// CORRECT — annotate the type
val empty: List[Int] = List()
val empty2 = List.empty[Int]

Why Explicit Types Are Still Useful

Even though Scala can infer types, explicit annotations serve important purposes:

Documentation

// Less clear — what does this return?
def compute(data: List[Double]) = data.map(x => x * 1.1).filter(_ > 5.0)

// Clear intent
def compute(data: List[Double]): List[Double] = data.map(x => x * 1.1).filter(_ > 5.0)

Catching Bugs Early

// Without annotation — bug goes unnoticed
def discount(price: Double) = price * 0.9  // returns Double, expected Int?

// With annotation — compiler catches the mismatch
def discount(price: Double): Int = price * 0.9  // Error: found Double, required Int

Best Practices


Omit type annotation when:           Add type annotation when:
──────────────────────────────────   ────────────────────────────────────
Simple val with obvious literal       Function return types (public API)
Local variables inside a function     Recursive functions
Short lambda expressions              Empty collection initialization
Results of transformation chains      When type is not obvious from context

Type Ascription

You can manually assert a type using a colon annotation. This is called type ascription:

val x = 42: Double        // forces x to be Double (42.0)
val y = List(1, 2): Seq[Int]  // treats the List as a Seq

// Useful when passing to a function expecting a supertype
def process(items: Seq[Int]): Unit = println(items.sum)
process(List(1, 2, 3): Seq[Int])  // explicit ascription

Type inference makes Scala code concise without sacrificing safety. The compiler does the bookkeeping so you can focus on logic. When in doubt, add a type annotation — it costs nothing and makes your code easier to read and maintain.

Leave a Comment

Your email address will not be published. Required fields are marked *