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.
