Scalable.FYI
ServicesBlogAbout UsCONTACTS

Why Use Scala / Type Inference

Engineering Team · 3 min read

2026-06-18

Why Use Scala / Type Inference

Introduction

"Static vs. dynamic" and "verbose vs. terse" get conflated constantly, and they're thought as independent axes that cannot coexist in the same plane. Python is dynamic and terse. Java is static and verbose. The interesting question is whether you can be both static and terse: full compile-time type safety, without every line reading like a full type declaration.

Let's look at the same small function across all four languages: given a list of items in a shopping cart, sum up the prices within each category, and hand back a lookup by category name and total price.

Python: optional, often skipped entirely

Python type hints are optional at the language level: unannotated code runs exactly like annotated code. So hint coverage varies wildly across a codebase, and a function's real contract is usually something you discover by reading the implementation.

Python
def category_totals(items):   # perfectly valid Python. What's "items"?
                              # A list? A tuple? Of what type?
                              # What comes back, a dict keyed and valued
                              # by what? Nothing here says it, and nothing
                              # enforces it even if hints were added later.
    totals = {}
    for item in items:
        totals[item.category] = totals.get(item.category, 0) + item.price
    return totals

TypeScript: structural, with escape hatches

TypeScript infers return types well and structurally, which is a better experience than Python. But it is gradual by design, and any is always there to turn checking off for whatever it touches, silently, without so much as a lint warning under a lot of configurations.

TypeScript
function categoryTotals(items: CartItem[]) {
  const totals: Record<string, number> = {};
  for (const item of items) {
    totals[item.category] = (totals[item.category] ?? 0) + item.price;
  }
  return totals; // return type correctly inferred: Record<string, number>
}

// But this compiles identically, and silently opts out of checking:
function categoryTotalsUnsafe(items: any) {
  const totals: any = {};
  for (const item of items) {
    totals[item.category] = (totals[item.category] ?? 0) + item.price;
  }
  return totals;
}

Java: static, and it shows

Java's var (Java 10+) removes some of the redundancy on the left-hand side of a local variable, but method signatures/parameters/return types still need every type spelled out by hand, in full, every time, and var turns out to have its own limits as well.

Java
Map<String, Double> categoryTotals(List<CartItem> items) {
    var totals = new HashMap<String, Double>();     // var helps here...
    for (var item : items) {                        // ...and here...
        totals.merge(item.category(), item.price(), Double::sum);
    }
    return totals;
}
// ...but the method's own signature, Map<String, Double>, still needs
// spelling out in full, and it isn't optional inside the body either:
// drop the <String, Double> from "new HashMap<>()" thinking var will
// infer it the way it does everywhere else, and the diamond silently
// falls back to HashMap<Object, Object> instead. That's not just less
// safe, it stops compiling outright: Double::sum no longer matches,
// and the return statement no longer matches Map<String, Double>.

Scala: fully static, inferred where it can prove it

Scala asks for far less than Java, with one firm requirement: parameter types stay explicit. A function's inputs are its contract, and inferring them from usage would make signatures depend on call sites, which gets confusing fast. Local variables and return types are inferred nearly always, though occasionally the compiler needs a hint. And unlike TypeScript, there is no any waiting to switch checking off.

Scala
def categoryTotals(items: List[CartItem]) =
  items.groupMapReduce(_.category)(_.price)(_ + _)

// The return type annotation above is fully inferred by the Scala
// compiler into a Map[String, Double]: no HashMap to construct, no diamond
// to get half right, no fallback type waiting to swallow a typo.

Compare that to Java's version: same input, same output, and Scala never once has to spell out Map[String, Double], let alone twice. Scala doesn't need var as a workaround either, because local inference was never opt-in to begin with, and there's no empty-diamond trap for it to half-solve.

Explicit where it matters

Idiomatic Scala still annotates public API boundaries, such as the return type on a library's public method. The compiler does not need it, but a human reading the signature does, so they can see what comes back without opening the implementation. The point is not "never write a type annotation". It is that an annotation becomes a choice you make for the reader, not a tax you pay on every line because otherwise the code won't compile.

Inference and token budgets

This ties back to the token-budget point we made about conciseness in general. Every line of redundant type annotation an AI agent has to read and reproduce, Java's Map<String, Double> here, spelled out as both the return type and again inside new HashMap<String, Double>(), is tokens spent restating information the compiler could derive on its own. It's also a sharper failure mode than it looks: an agent that drops the diamond's type arguments, expecting var to infer them the way it does everywhere else, doesn't get a clear error at the point of the mistake. It gets a confusing one two lines later, at the merge call and the return statement, for a cause that's no longer visible on screen. Scala's inference doesn't come at the cost of soundness the way TypeScript's any does, either: an agent can lean on inferred types with real confidence that they're checked, not just present-looking.

The Python side of this is a different but related risk: unannotated code gives an AI agent no compiler feedback loop at all. It can generate a function that passes a dict where a CartItem was expected, and nothing (not the language, nor the editor, usually) flags it before the code runs against real data.