Scala Variables, Types, and Type Inference

TT
TopicTrick

Scala's type system is one of its defining strengths. Unlike dynamically typed languages where type errors appear at runtime, Scala catches them at compile time — and unlike Java, it does so without requiring you to write types everywhere. Type inference deduces most types automatically, giving you safety without verbosity.

val vs var

Scala has two kinds of variable declarations:

scala
val name = "Alice"    // immutable — cannot be reassigned
var count = 0         // mutable — can be reassigned

val is the default choice in idiomatic Scala. Immutability eliminates an entire class of bugs (unexpected mutation) and makes concurrent code easier to reason about. Use var only when mutation is genuinely necessary — it is relatively rare in well-written Scala.

Attempting to reassign a val is a compile error:

scala
val x = 10
x = 20  // error: reassignment to val

Explicit Type Annotations

Scala infers types automatically, but you can always annotate explicitly:

scala
val name: String = "Alice"
val age: Int = 30
val pi: Double = 3.14159
val active: Boolean = true

The syntax is name: Type = value. Explicit annotations are useful for documentation, for cases where inference is ambiguous, and when defining API boundaries.

Type Inference

Scala's compiler infers the type from the right-hand side expression:

scala
val message = "Hello"      // inferred: String
val count = 42             // inferred: Int
val ratio = 3.14           // inferred: Double
val items = List(1, 2, 3)  // inferred: List[Int]

Type inference works through function return types too:

scala
def double(n: Int) = n * 2   // return type inferred as Int
def greet(name: String) = s"Hello, $name"  // inferred: String

For public APIs, it is good practice to annotate return types explicitly — it documents intent and catches errors when the implementation changes.

The Scala Type Hierarchy

Scala's type hierarchy unifies primitives and objects:

text
Any
├── AnyVal  (value types — no heap allocation)
│   ├── Int, Long, Double, Float, Short, Byte
│   ├── Char, Boolean
│   └── Unit
└── AnyRef  (reference types — heap allocated, = java.lang.Object)
    ├── String, List, Array, ...
    └── All class instances

Any is the root of all Scala types. Every value is an Any.

AnyVal covers the JVM primitive types. Scala represents them as Java primitives at the bytecode level for efficiency, but treats them as objects in source code.

AnyRef is equivalent to Java's Object. All class instances, collections, and strings are AnyRef.

Unit is the Scala equivalent of void — the type of expressions that produce no meaningful value (like println). Its only value is ().

Nothing is at the bottom of the hierarchy — a subtype of every type. Methods that always throw an exception have return type Nothing.

Null is the type of the null reference — subtype of all AnyRef types. In idiomatic Scala, null is avoided entirely in favour of Option.

Numeric Types

scala
val i: Int = 2_147_483_647      // 32-bit, max Int
val l: Long = 9_223_372_036L    // 64-bit, L suffix
val d: Double = 3.14159         // 64-bit float
val f: Float = 3.14f            // 32-bit float, f suffix
val b: Byte = 127               // 8-bit
val s: Short = 32767            // 16-bit

Scala does not perform implicit numeric widening — val x: Double = 42 works (Int literal fits in Double), but you cannot silently pass an Int where a Long is expected in generic contexts.

String Interpolation

Scala provides three string interpolator prefixes:

s-interpolation — evaluates expressions inside ${}:

scala
val name = "Alice"
val age = 30
println(s"$name is $age years old")          // Alice is 30 years old
println(s"Next year: ${age + 1}")            // Next year: 31

f-interpolation — like s but with printf-style format specifiers:

scala
val pi = 3.14159
println(f"Pi is approximately $pi%.2f")     // Pi is approximately 3.14

raw-interpolation — no escape processing:

scala
println(raw"Line 1
Line 2")    // Line 1
Line 2 (no newline)

Frequently Asked Questions

Q: When should I use var instead of val? Use var when you genuinely need mutation — typically for performance-critical local loops, mutable accumulators inside a method, or interoperating with mutable Java libraries. In most application code, val with immutable data structures is the right default. If you find yourself reaching for var often, consider whether you can express the same logic with functional collection operations.

Q: What is the difference between Int and java.lang.Integer in Scala? Scala's Int compiles to the Java primitive int at the bytecode level — it is not boxed by default. When you use Int in a generic context (e.g., List[Int]), the JVM boxes it to java.lang.Integer automatically. Scala handles this transparently. You rarely need to use java.lang.Integer directly in Scala code.

Q: How does Scala handle null compared to Java? Scala has null for JVM compatibility, but idiomatic Scala avoids it entirely. Instead of returning null when a value might be absent, Scala uses Option[T] — either Some(value) or None. This forces callers to handle the absence case explicitly, eliminating NullPointerExceptions. The Option type is covered in depth in the Functional Programming phase.


Part of the Scala Mastery Course — Module 3 of 22.