Scala lets you leave out most types, and most of the time the compiler fills them in exactly as you expected. The rest of the time you get an inferred Any, a Product with Serializable in a public signature, a lambda rejected for a missing parameter type, or a fold that refuses to compile because the accumulator's type is Nil.type. These failures look random until you know the algorithm. Once you do, each one has an obvious cause and a one-line fix.
This article explains how Scala infers types: what local inference means and why Scala chose it over the whole-program inference of ML and Haskell, how expected types flow down into arguments and lambdas, how type arguments are solved from constraints, how branches are joined into a least upper bound, when singleton and union types are widened, and what changed between Scala 2 and Scala 3. It ends with an annotation policy you can apply to a codebase. Variance, bounds and higher-kinded types are covered in the Scala type system.
Local inference, and why Scala does not use Hindley-Milner
Languages in the ML family use Hindley-Milner inference: the compiler collects equality constraints across a whole definition group and unifies them, so even parameter types are deduced from how the body uses them.
Hindley-Milner relies on the absence of subtyping. With subtyping, constraints become inequalities, a variable can have many valid solutions, and the principal-type guarantee that makes global inference predictable disappears. Scala has subtyping, overloading and implicits, so it uses local type inference instead. The compiler type-checks one expression at a time, in source order, using only types already known at that point, and never revisits a decision based on later code.
Three ground rules follow directly. Method parameters are never inferred from their uses, so def f(x) = x + 1 is a syntax error. A recursive method must declare its result type, because the compiler would need the result to type the body that produces it. And a value's type is decided at its definition, not by how it is used later.
val n = 42 // Int: synthesized from the literal, then widened from 42
final val k = 42 // 42: a final val keeps the literal (singleton) type
val xs = List(1, 2, 3) // List[Int]: A inferred from the arguments
val ys = xs.map(x => x * 2.5) // List[Double]: x typed from map's expected function type
val empty = List() // List[Nothing]: no constraint on A at all
val f = (x: Int) => x + 1 // Int => Int: parameter annotated, result synthesized
def twice(x: Int) = x * 2 // result type inferred: Int
// def fact(n: Int) = if n == 0 then 1 else n * fact(n - 1)
// error: recursive method fact needs result type
def fact(n: Int): Int = if n == 0 then 1 else n * fact(n - 1)
Two directions: synthesis and expected types
Scala's inference is bidirectional. In the upward direction, the compiler synthesizes a type from an expression's parts: a literal 42 is an Int, a + b has the result type of the + method selected on a, and a block has the type of its last expression. In the downward direction, the context supplies an expected type, sometimes called the prototype, which the expression must conform to and which can steer how it is typed.
Expected types come from annotations, parameter types and declared results, and matter most for function literals. A lambda x => x + 1 cannot be typed bottom-up because nothing says what x is. When it appears where a Int => Int is expected, the compiler reads the parameter type from the expected type and then synthesizes the body. That is why xs.map(x => x + 1) compiles and val f = x => x + 1 does not: the first has an expected type from map's signature, the second has none.
The same mechanism lets a lambda implement an expected SAM interface such as Runnable, and lets an Int literal become a Double.
Type arguments: constraints and instantiation
When you call a polymorphic method without explicit type arguments, the compiler creates a type variable for each type parameter and records constraints as it checks the arguments. Calling List(1, 2, 3) produces the lower bounds Int <: A three times. Calling map with x => x * 2.5 produces Double <: B once the body is synthesized. Declared bounds such as A <: Comparable[A] add upper bounds.
Once the arguments are checked, the solver instantiates each variable. As a rule it picks the smallest type that satisfies its lower bounds, the least upper bound of everything that flowed in, because a more precise type is more useful to the caller. If a variable has no lower bound, it is instantiated to Nothing, which is how List() becomes List[Nothing]. Variance and the expected result type can tilt the choice; when a type variable appears only contravariantly, the solver may maximize instead.
The crucial detail is when instantiation happens. Scala solves type variables per argument list. If a method has several parameter lists, the variables constrained by the first list are fixed before the second list is checked, and that fixed type becomes the expected type for any lambdas there. This is the reason idiomatic Scala libraries put the shape-determining argument in its own first list, a design choice explored in higher-order functions.
Worked example: the foldLeft trap
The most common inference error in Scala comes from a method that uses exactly this rule. foldLeft[B](z: B)(op: (B, A) => B): B fixes B from the zero value alone. Pass Nil and the most precise type of that expression is the singleton object type Nil.type, so B becomes Nil.type and the lambda is required to return it:
val xs = List(1, 2, 3)
// B is fixed by the FIRST argument list, before the lambda is typed.
// Nil has type Nil.type, so B := Nil.type and the lambda must return Nil.type.
xs.foldLeft(Nil)((acc, x) => x :: acc)
// error: type mismatch; found: List[Int], required: Nil.type
// Fix 1: give the zero the type you mean.
xs.foldLeft(List.empty[Int])((acc, x) => x :: acc)
// Fix 2: supply the type argument explicitly.
xs.foldLeft[List[Int]](Nil)((acc, x) => x :: acc)
// The same trap with Option:
xs.foldLeft(None)((acc, x) => Some(x)) // B := None.type, fails
xs.foldLeft(Option.empty[Int])((acc, x) => Some(x))Read the error the way the compiler reached it. It says the lambda produced List[Int] where Nil.type was required, so ask where the required type came from: the first argument list. The fix is never to annotate the lambda; it is to make the zero value carry the type you want, with List.empty[Int], Option.empty[Int], Map.empty[K, V] or an explicit type argument. The same pattern appears with foldRight, scanLeft, getOrElse and any API that takes a seed before a function.
Joining branches: least upper bounds
An if, a match or a collection literal with several elements needs one type that covers every alternative. The compiler computes a least upper bound, the most specific common supertype. For two case classes extending a sealed trait, that common supertype includes not only the trait but every other parent the classes share, and all case classes extend Product and Serializable.
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
val s = if (cond) Circle(1) else Square(2)
// Scala 2: Product with Serializable with Shape
// Scala 3: Shape (Product and Serializable are transparent and dropped)
val t = if cond then 1 else "one"
// Scala 3: Int | String (Any is transparent, so the union is kept)
// Scala 2: Any
// Scala 2 fix that also documents intent:
sealed trait Shape extends Product with Serializable
// Numeric mixing
val n: Int = 3; val d: Double = 1.5
List(n, d) // Scala 2: List[Double] (weak conformance)
// Scala 3: not List[Double]; only Int LITERALS are adapted
List(1, 2.5) // both: List[Double]In Scala 2 the result leaks into inferred val types, element types and public signatures. The standard fix is to declare the sealed trait itself as extends Product with Serializable, so the join collapses back to Shape.
Scala 3 instead drops transparent traits and classes from inferred joins when other parents remain. Any, AnyVal, Matchable, Product, Object, Comparable and Serializable are transparent by default, and you can declare your own transparent trait. A union arising from branches is soft: it is widened to its visible join when one exists, so the shape example infers Shape. When the only common parents are transparent, as with 1 and "one", the union Int | String is kept rather than collapsing to Any.
Widening: singletons, literals and numbers
Every literal has a singleton type: 42 can be typed as the type 42, and a stable path x as x.type. If inference kept those precise types, var n = 0 could never be reassigned. So the compiler widens singleton types to their underlying type when it infers the type of a val, var or method result, unless you ask otherwise. Declaring final val k = 42 keeps the literal type, which is what makes k a compile-time constant usable in annotations and inline code, and an explicit singleton ascription such as val mode: "fast" = "fast" does the same.
Object types such as Nil.type and None.type are not literal types and are not widened away, which is why the foldLeft trap exists.
Numeric mixing changed between versions. Scala 2 used weak conformance, treating Int as conforming to Double for inference, so a list mixing an Int variable and a Double variable was a List[Double]. Scala 3 dropped weak conformance and kept a single rule: integer literals are adapted to the numeric type the context needs. A list of an Int literal and a Double is still List[Double], but the same list built from variables is no longer List[Double]. If a Scala 3 migration suddenly changes such an element type, this is usually why; convert explicitly with .toDouble.
Inference and implicits
Implicit and given search runs after the explicit arguments are typed, and a found instance can fix remaining type variables; that is how xs.sorted obtains an Ordering[Int]. A variable inferred too wide makes the search look for an Ordering[Any] that does not exist, and the error names the missing instance rather than the inference behind it. When such an error mentions Any or Nothing, fix the inference first. The search rules themselves are in implicit resolution and the Scala 3 syntax in Scala 3 givens.
Failure modes and how to diagnose them
- Silent
Any. A branch returnsUnitor a different type by mistake and the join becomesAnyorAnyVal. Everything compiles until a later call fails to find a method. In Scala 2,-Xlint:infer-anywarns when a type argument is inferred asAny. - Leaky public types. A method without a result type exposes whatever the body happens to produce, such as a mutable collection type or a long intersection. Changing the body later changes the signature, can break binary compatibility, and forces every caller to recompile.
- Missing parameter type. A lambda with no expected type, typically assigned to an unannotated
valor passed to an overloaded method. Annotate thevalor the lambda parameter. - Seed-before-function traps.
Nil,Noneor a too-narrow literal as the first argument of a fold-like API. Use.emptyconstructors.
To see what the compiler decided, hover in Metals or IntelliJ, enable inferred-type hints in the editor, or ascribe a deliberately wrong type such as val x: Int = expr and read the type it reports for expr. Read every error message by asking which earlier argument list or definition fixed the type it complains about.
An annotation policy that scales
Inference is a tool for removing noise in local code, not a way to avoid deciding on types. A policy that works in large codebases is short. Annotate the result type of every public and protected method and every non-private val. Annotate recursive methods, which the compiler requires anyway. Give type arguments to empty collections and seeds. Leave local vals, lambda parameters inside library calls and private helpers unannotated unless the type is surprising. In Scala 3, also annotate given instances, since their declared type is what the search matches.
Scalafix rules can add explicit result types to public members mechanically. Doing that before a Scala 3 upgrade pins every signature, so inference changes between versions cannot alter public APIs.
What to do next
- Run a build with
-Xlint:infer-anyon Scala 2, or review inferred types in your IDE on Scala 3, and fix every unintendedAnyorAnyVal. - Search for
foldLeft(Nil),foldLeft(None)andgetOrElse(Nil)patterns and replace them with typed.emptyseeds. - On Scala 2, add
extends Product with Serializableto every sealed ADT root; on Scala 3, mark your own marker traitstransparentwhere appropriate. - Add explicit result types to all public members, using a Scalafix rule to do it mechanically.
- Before migrating to Scala 3, grep for collections built from mixed numeric variables, which stop being
List[Double], and add explicit conversions. - When writing your own higher-order API, put the type-determining arguments in an earlier parameter list than the functions that use them.