Type Inference

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Scala 3.10 (3.10.0, released 2026-10-12) and every example was compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). The runnable programs and the error demos were also compiled with 3.9.0 (the current LTS) and 3.3.8 (the previous LTS) and, where the text says so, with Scala 2.13.18 and other 3.x releases; the differences between versions are called out where they occur.

Scala is statically typed, but you write far fewer types than in Java 8 or C++98 because the compiler infers them. The inference is local: it works inside one definition, left to right, using the types of the values you wrote and the type the surrounding code expects. It never looks at how other code uses a value in order to decide that value’s type. This page explains what is inferred, where inference gets help from the expected type, where you are required (or well advised) to write a type, and what changed from Scala 2. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails. For the types themselves see Type Hierarchy & Matchable, for the type parameters that inference fills in see Generics & Bounds, and for union types see Union & Intersection Types.

What gets inferred

You can leave out the type of a val, var, def result and given, of the type arguments of a generic call or constructor, and of the parameters of a lambda whose expected type is known. You cannot leave out the type of a method parameter: the compiler never infers those.

//> using scala 3.10.0

package trailhead

enum Difficulty:
  case Easy, Moderate, Hard
  case Alpine(grade: Int)

case class Trail(name: String, lengthKm: Double, ascentM: Int, difficulty: Difficulty)

@main def run(): Unit =
  val name = "Ridge"                       // String
  val length = 8.5                         // Double
  val trails = List(Trail("Ridge", 8.5, 900, Difficulty.Hard), Trail("Meadow", 3.0, 120, Difficulty.Easy))
  val byName = trails.map(t => t.name -> t).toMap      // Map[String, Trail]
  val grades = trails.map(_.difficulty)                // List[Difficulty]
  val mixed = List(1, 2.5)                             // List[Double]
  val odd = List(1, "two")                             // List[Int | String]
  val flag = if length > 5 then 1 else "short"         // Int | String
  val n: Long = 1 + 2                                  // the expected type steers the literal
  val e = List()                                       // List[Nothing]
  def describe(t: Trail) = s"${t.name}: ${t.lengthKm} km"   // result type String
  println(describe(trails.head))
  println(mixed)
  println(odd)
  println(flag)
  println(n)
  println(e)
  println(byName.keys)
  println(grades)
Ridge: 8.5 km
List(1.0, 2.5)
List(1, two)
1
3
List()
Set(Ridge, Meadow)
List(Hard, Easy)

The comments hold the types the compiler chose. The way to see them for yourself is to give the value an incompatible expected type and read the error, or to hover in an IDE (see IDEs). Assigning to Nothing always fails and always prints the type found:

//> using scala 3.10.0

case class Box[A](a: A)

object Reveal:
  val a = 1
  val b = List(1, 2.5)
  val c = List(1, "two")
  val d = if a > 0 then 1 else "x"
  val e = List()
  val f = Map(1 -> "one", 2 -> "two")
  var obj = null
  final val g = 3
  val p = Box(1)
  val q = Box(Box("s"))
  val ra: Nothing = a
  val rb: Nothing = b
  val rc: Nothing = c
  val rd: Nothing = d
  val re: Nothing = e
  val rf: Nothing = f
  val ro: Nothing = obj
  val rg: Nothing = g
  val rp: Nothing = p
  val rq: Nothing = q

The compiler reports one Found: line per assignment (the file path and the repeated Required: Nothing lines are left out here):

Found:    (Reveal.a : Int)
Found:    (Reveal.b : List[Double])
Found:    (Reveal.c : List[Int | String])
Found:    (Reveal.d : Int | String)
Found:    (Reveal.e : List[Nothing])
Found:    (Reveal.f : Map[Int, String])
Found:    (Reveal.obj : Null)
Found:    (Reveal.g : (3 : Int))
Found:    (Reveal.p : Box[Int])
Found:    (Reveal.q : Box[Box[String]])

Each line is one rule:

Expression Inferred type Why

val a = 1

Int

A literal has a singleton type (1) that is widened to its class type when the value is not final and is not constant. final val g = 3 keeps the literal type (3 : Int).

List(1, 2.5)

List[Double]

The numeric literal 1 is adapted to the Double the other element requires.

List(1, "two")

List[Int | String]

There is no useful common superclass of Int and String, so the compiler keeps a union type.

Map(1 -> "one", ...)

Map[Int, String]

Type arguments of Map.apply are solved from the pairs.

List()

List[Nothing]

No argument constrains the element type, so it is the bottom type, Nothing.

var obj = null

Null

null has the type Null; see The Nothing and Null traps.

Box(Box("s"))

Box[Box[String]]

The type parameter of a class is inferred from the constructor arguments.

The expected type flows into lambdas

The most useful form of inference is bidirectional: when an expression is checked against an expected type, that type is pushed into the expression. A lambda passed to a method whose parameter type is known gets its own parameter types for free.

//> using scala 3.10.0

@main def run(): Unit =
  val xs = List(1, 2, 3)
  val a = xs.map(x => x * 2)            // x: Int comes from the expected function type
  val b = xs.foldLeft(0)((acc, x) => acc + x)
  val c = xs.foldLeft(List.empty[Int])((acc, x) => x :: acc)
  println(a)
  println(b)
  println(c)
  val d = xs.sortWith((l, r) => l > r)
  println(d)
  val f: Int => Int = x => x + 1        // the declared type gives x its type
  println(f(1))
List(2, 4, 6)
6
List(3, 2, 1)
List(3, 2, 1)
2

Without any expected type there is nothing to flow in, and the compiler asks for the parameter type:

//> using scala 3.10.0

@main def run(): Unit =
  val f = x => x + 1
  println(f(1))
./C.scala:4:11
Missing parameter type

I could not infer the type of the parameter x
  val f = x => x + 1
          ^

The fix is val f: Int => Int = x => x + 1 or val f = (x: Int) => x + 1. This is why map(x => ...) needs no annotation and a stand-alone lambda does.

The same mechanism sets the type of literals: in val n: Long = 1 + 2 the expected type Long makes the literals Long.

Type arguments are solved across the call

For a generic method the compiler collects constraints on each type parameter from the arguments and the expected type and then picks a solution. In Scala 3 this works across parameter lists as well: a later argument can constrain a type parameter that an earlier argument left open.

//> using scala 3.10.0

@main def run(): Unit =
  val xs = List(1, 2, 3)
  val r = xs.foldLeft(Nil)((acc, x) => x :: acc)
  println(r)
List(3, 2, 1)

On Scala 3.10.0 and 3.9.0 this compiles and B is solved as List[Int]; it also compiled on 3.4.3, 3.5.2, 3.6.4, 3.7.4 and 3.8.4. The 3.3 LTS line (checked with 3.3.8) rejects it with the same kind of error as Scala 2 ("Found: List[Int] Required: scala.collection.immutable.Nil.type"). The same program on Scala 2.13.18 fails:

./P.scala:5:44
type mismatch;
 found   : List[Int]
 required: collection.immutable.Nil.type
    val r = xs.foldLeft(Nil)((acc, x) => x :: acc)
                                           ^^^^^^

Scala 2 fixed B to the type of the first argument, Nil.type, and then rejected the lambda. The workaround that older books show, foldLeft(List.empty[Int]) or foldLeft(Nil: List[Int]), still works and is still clearer when the seed is not obviously the right type.

Joining branches and elements

When the compiler has to find one type for several expressions, such as the two branches of an if, the cases of a match or the elements of a collection literal, it takes their join. Scala 3 is willing to answer with a union type when no better common type exists.

//> using scala 3.10.0

enum Difficulty:
  case Easy, Moderate, Hard
sealed trait Segment
case class Path(km: Double) extends Segment
case class Ridge(km: Double) extends Segment

object Joins:
  def pick(c: Boolean) = if c then Path(1.0) else Ridge(2.0)
  def diff(c: Boolean) = if c then Difficulty.Easy else Difficulty.Hard
  def opt(c: Boolean) = if c then Some(1) else None
  def mix(c: Boolean) = if c then 1 else 2.5
  def lit(c: Boolean) = if c then "a" else "b"
  def seq(c: Boolean) = if c then List(1) else Vector(1)
  val f = Seq(Path(1.0), Ridge(2.0))

The inferred result types, read the same way as above:

Definition Inferred type

pick

Segment

diff

Difficulty

opt

Option[Int]

mix

Double

lit

String

seq

scala.collection.immutable.AbstractSeq[Int]

f

Seq[Segment]

So two case classes of one sealed trait join to the trait (the Product and Serializable parents that every case class has do not appear in the inferred type), two enum cases join to the enum, and Some and None join to Option. A List and a Vector join to an internal class, AbstractSeq, that you did not mean to expose; the next section shows how to avoid that. In the examples above the union appears only where the common supertype would be as vague as Any, as with Int | String.

Unions are not an error to be avoided. They are the right answer for List(1, "two"), and a var can be reassigned anywhere inside the union:

//> using scala 3.10.0

def same[A](a: A, b: A): List[A] = List(a, b)

@main def run(): Unit =
  val length = 2
  println(same(1, "x"))                  // List[Int | String]
  var m = if length > 1 then 1 else "s" // Int | String
  println(m)
  m = 2
  m = "t"
  println(m)
List(1, x)
1
t

When you must write a type, and when you should

Recursive methods

A recursive method cannot be inferred, because its result type depends on a call to itself. The compiler stops with the error that carries the code E044:

//> using scala 3.10.0

def fact(n: Int) = if n <= 1 then 1 else n * fact(n - 1)

@main def run(): Unit =
  println(fact(5))
./D.scala:7:46
Overloaded or recursive method fact needs return type

 Run with -explain-cyclic for more details.
def fact(n: Int) = if n <= 1 then 1 else n * fact(n - 1)
                                             ^

The same message appears for overloaded methods, because the compiler type-checks overload alternatives while resolving calls. Write def fact(n: Int): Int = .... In practice this requirement is rarely a nuisance, because the recursive and overloaded methods are exactly the ones whose signatures are worth reading.

Public API

The style guide says to use inference freely for private fields and local values and to annotate the result type of every public method. The reason is not style but stability: an inferred type is part of your public interface, and it changes whenever the body changes.

//> using scala 3.10.0

object Planner:
  // Looks like Seq[Int], but is AbstractSeq[Int] -- an implementation detail leaks out
  def stops(c: Boolean) = if c then List(1) else Vector(1)

  // The public contract is written down; the body must now conform to it
  def stopsTyped(c: Boolean): Seq[Int] = if c then List(1) else Vector(1)

If a later edit changes if c then List(1) else Vector(1) into if c then List(1) else Nil, the inferred type of stops changes to List[Int] and a client that relied on the old type may no longer compile. An annotated method turns the same edit into a local compile error in the method that was edited. Annotations also speed compilation a little, because the compiler need not type-check the body to learn the signature.

When a member overrides another and you give no result type, Scala takes the overridden type into account:

//> using scala 3.10.0

trait Source:
  def distance: Double
class Lake extends Source:
  def distance = 2

@main def run(): Unit =
  val l = Lake()
  val x: Nothing = l.distance
./G.scala:8:20
Found:    Double
Required: Nothing
  val x: Nothing = l.distance
                   ^^^^^^^^^^

Lake.distance is a Double even though its body is the literal 2. Scala 2.13.18 reports the same Double, and Scala 3.3.8 and 3.10.0 agree. Explicit result types in classes and traits remain advisable, because the reader should not have to look at the parent to learn the type.

Which definitions should carry a type:

  • every public or protected def, val and given, and every member of a published library

  • every recursive or overloaded method (required)

  • values whose initializer is a conditional or a match that may join to something unintended

  • a var that will later hold a more general type than its initial value, such as var best: Option[Trail] = None

Inference is the better choice for private members, local values, lambda parameters and anything whose value shows the type, as in val trails = List(Trail("Ridge", 8.5, 900, Difficulty.Hard)). For the given definitions that inference also touches see Givens & Using Clauses.

The Nothing and Null traps

Two inferred types are almost never what you meant: Nothing and Null. Both are bottom types, so a value of one of them cannot be replaced by a more useful one. List() is List[Nothing]: it works wherever a List[Int] is expected because of covariance, but a variable that stores only that type is useless. var obj = null is worse:

//> using scala 3.10.0

@main def run(): Unit =
  var obj = null
  obj = new AnyRef
./N.scala:4:9
Found:    Object
Required: Null
  obj = new AnyRef
        ^^^^^^^^^^

The only value of type Null is null. Write var obj: AnyRef | Null = null or, better, var obj: Option[AnyRef] = None. For List() use List.empty[Trail] or an annotated value: val none: List[Trail] = Nil.

Overload resolution in Scala 3

Overloaded methods are resolved before arguments are fully typed, so inference interacts with them. Scala 3 improves three things relative to Scala 2: it looks at all argument lists, it types lambdas with missing parameter types in the first argument list when the rest of the call selects the alternative, and it no longer drops an alternative because it has default arguments.

//> using scala 3.10.0
def f(x: Int)(y: String): Int = 0
def f(x: Int)(y: Int): Int = 1
def g(x: Int, f2: Int => Int) = f2(x)
def g(x: String, f2: String => String) = f2(x)
def h(x: Int) = "plain"
def h(x: Int, y: Int = 0) = "defaults"

@main def run(): Unit =
  println(f(3)(""))
  println(f(3)(4))
  println(g("a", _.toUpperCase))
  println(g(2, _ * 2))
  println(h(1))
0
1
A
4
plain

The same f and g, wrapped in an object and compiled with Scala 2.13.18, fail:

./H.scala:8:13
ambiguous reference to overloaded definition,
both method f in object H of type (x: Int)(y: Int): Int
and  method f in object H of type (x: Int)(y: String): Int
match argument types (Int)
    println(f(3)(""))
            ^
./H.scala:9:20
missing parameter type for expanded function ((<x$1: error>) => x$1.toUpperCase)
    println(g("a", _.toUpperCase))
                   ^

When several alternatives remain after the first argument list, resolution is retried with the second, and so on. A lambda with missing parameter types is approximated as ? => ? while the alternatives are filtered, and its parameter types are filled in once one alternative survives. Both changes are why overloaded extension methods, and overloads that take callbacks, work better than in Scala 2.

Pitfalls

  • A lambda without context. val f = x => x + 1 fails with "Missing parameter type". Give the value a function type.

  • Widening that surprises. val xs = List(1, 2.5) is List[Double], not List[Int | Double]; if you want a union, annotate: val xs: List[Int | Double] = List(1, 2.5).

  • Unintended join. A conditional of two collection types infers an internal class, as AbstractSeq[Int] above.

  • Nothing from empty generics. val e = List() is List[Nothing]; Map() likewise has no key or value type.

  • Inference does not look forward. A val is typed from its right-hand side only, never from how it is used later. In var best = None, the type is None.type, and assigning Some(1) later fails with "Found: Some[Int] Required: None.type".

  • Compile times. Large expressions without annotations can take long to type-check; an annotation on a long chain is a cheap fix.

Java, Kotlin and Python

Java 10 added var (JEP 286) for local variables only. Java infers the type of a local from its initializer, never the result type of a method or the type of a field, and a lambda parameter type is inferred from the target type as in Scala (JEP 323 allows var for lambda parameters). Scala infers all of these. Kotlin is closer to Scala: it infers val, var, expression-bodied function result types and lambda parameters. A block-bodied Kotlin function returns Unit unless you write a result type, and a recursive expression-bodied function needs one. Kotlin has no union types, so the common supertype of two unrelated branches is Any. Python and JavaScript have no static inference to compare, though Python type checkers such as mypy and pyright do infer local types. See also Java: Generics and Kotlin: Generics & Variance.

Where the books are outdated

  • Scala 2 books and blog posts say that foldLeft(Nil)(...) and similar calls need a type ascription. Scala 3 solves them from 3.4 on (shown above); on the 3.3 LTS line the Scala 2 workaround is still needed.

  • Posts written for Scala 2 say that overloaded methods cannot take lambdas with missing parameter types, or that f(x)(y) overloads are ambiguous. Both work in Scala 3.

  • Scala 2 posts use List[Any] to explain List(1, "two"). In Scala 3 the element type is the union Int | String.

  • The Tour of Scala states that the compiler "never infers method parameter types". That is still true; it is lambda parameters, not method parameters, that are inferred.

  • Material from the Scala 2 era describes the inferred types of var x = null and recursive methods correctly, and those rules are unchanged.

  • The Scala 3 reference page "Changes in Type Inference" holds only links to two talks (Guillaume Martres, 2019, and Aleksander Boruch-Gruszecki, 2019); the working description of the algorithm is in the talks and in the language specification.

Scala 3.9 LTS versus 3.10

  • Everything on this page behaves the same on 3.9.0 and 3.10.0: the runnable programs printed identical output and the error demos reported the same errors.

  • The 3.3 LTS line (3.3.8) differs in two places: foldLeft(Nil)(...) does not compile there, and the missing-lambda-type error reads "Could not infer type for parameter x of anonymous function" instead of "Missing parameter type". The other examples gave the same results on 3.3.8.

  • Scala 2.13.18 differs in the places called out above: foldLeft(Nil), the f(x)(y) overload and the lambda in an overloaded call.