Type Hierarchy & Matchable
|
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 run with Scala 3.10.0 and Scala CLI
1.17.1 on JDK 26 (checked on 2026-10-15). Matchable, TypeTest, ClassTag and the checks described here have existed since Scala 3.0 (explicit nulls since 3.0 as an experimental option), so everything on the page also holds on the 3.9 LTS line. Scala has
a single type hierarchy with no split between primitives and objects: Int, a String, a case class and a function all
conform to Any. This page maps that hierarchy, shows what the two bottom types Nothing and Null are for, explains the
Matchable trait that Scala 3 inserted between Any and the rest, and covers the runtime side: casts, type tests, ClassTag,
TypeTest, erasure, and the Product interface that case classes and tuples share. Values and variables are on
Values, Variables & Literals, pattern matching proper on
Pattern Matching, opaque types on
Opaque Types & Value Classes, and tuples on
Tuples & Named Tuples. Java’s counterpart is described on
Java Generics and Kotlin’s on
Kotlin Null Safety.
The unified hierarchy
At the top of the hierarchy sits Any, the supertype of every type. Scala 3 adds Matchable directly below it: every type
that may be inspected by a pattern match or a type test is a subtype of Matchable. Below Matchable the hierarchy forks into
AnyVal, the value types that the JVM represents as primitives, and AnyRef, which is java.lang.Object on the JVM and
covers every class, trait, array and function. At the bottom are Nothing, a subtype of every type, and Null, a subtype of
every reference type.
The diagram abbreviates: Byte, Short and Float are further AnyVal children next to Int, and Nothing sits below every
type, not only the ones drawn. Trail stands for the case class of the running trailhead scenario, which extends Product
automatically. Under the -Yexplicit-nulls option (see Null and explicit nulls) Null is no longer a subtype of every
reference type: it becomes a regular class that extends AnyVal.
The compiler can prove each edge of the hierarchy, which makes it a good first program.
//> 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 hierarchy(): Unit =
val pico = Trail("Pico", 8.5, 900, Difficulty.Hard)
// The compiler proves each edge of the hierarchy.
summon[Int <:< AnyVal]
summon[Unit <:< AnyVal]
summon[Trail <:< AnyRef]
summon[Trail <:< Product]
summon[AnyVal <:< Matchable]
summon[AnyRef <:< Matchable]
summon[Matchable <:< Any]
summon[Nothing <:< Trail]
summon[Null <:< Trail]
val anything: Any = pico
val number: AnyVal = 42
println(anything.getClass.getName)
println(number.getClass.getName)
println(classOf[Unit])
println(((): Any).getClass.getName)
println((1: Long)) // Int widens to Long by numeric widening, not by subtyping
trailhead.Trail
java.lang.Integer
void
scala.runtime.BoxedUnit
1
The same hierarchy is why List[Any] can hold an Int, a String and a Trail at once, and why a method that returns
Any can be used wherever any value is accepted. Int <:< Long is not provable: the widening of Int to Long that the last
line relies on is a numeric conversion the compiler inserts, not subtyping.
[error] ./b/Err.scala:4:23
[error] Cannot prove that Int <:< Long.
[error] summon[Int <:< Long]
[error] ^
Any, AnyVal and AnyRef
Any declares the universal methods. == and != are final and call equals (with null handling), ## is the hash code
that agrees with == for boxed numbers, equals, hashCode and toString come from the JVM, getClass returns the runtime
class, and isInstanceOf[T] and asInstanceOf[T] are the primitives behind type tests and casts. AnyRef adds the reference
operations of java.lang.Object: eq and ne for reference identity, synchronized, and the monitor methods wait,
notify and notifyAll. eq is therefore not available on an Any:
[error] ./o/E.scala:5:11
[error] value eq is not a member of Any.
[error] An extension method was tried, but could not be fully constructed:
...
[error] println(a.eq(a))
[error] ^^^^
The value classes in AnyVal are Byte, Short, Int, Long, Float, Double, Char, Boolean and Unit. They are
final, cannot be extended and cannot be null. The compiler uses the JVM primitive (int, double and so on) wherever the static
type is exact, and boxes into java.lang.Integer and friends when the value is seen as Any, as a type parameter or as an
element of a generic collection. The getClass of a number held in an Any therefore reports the box, as the example above
showed (java.lang.Integer). Equality on Any is cooperative: == compares numbers by value regardless of the box, equals
does not.
//> using scala 3.10.0
//> using option -source:future
@main def anyOps(): Unit =
val a: Any = 1
val b: Any = 1L
println(a == b)
println(a.equals(b))
println(a.## == 1.##)
println(a.getClass)
println(a.isInstanceOf[Int])
println("x".eq("x"))
true
false
true
class java.lang.Integer
true
true
1 == 1L is true because == unboxes and compares numerically; Integer.valueOf(1).equals(Long.valueOf(1)) is false because
the boxes have different classes. Relying on equals for numbers held as Any is a pitfall; == is what you want. The last line
prints true because both literals are the same interned String instance, which is an accident of the JVM and not a
guarantee: use == for content equality and eq only when identity matters.
Unit is the type of (), the single value returned by methods that exist for their side effects. At run time a Unit
method returns void, and () is the single scala.runtime.BoxedUnit instance when it has to be boxed as an Any. A
method that is declared to return Unit discards whatever value its body computes; the related compiler warnings are
covered with the other options on
Compiler Options, Linting & Errors.
Source: Tour of Scala: Unified Types
Nothing and Null
Nothing has no values. Its role is to be a subtype of every type, so an expression that never completes normally can be
used wherever any type is expected. A method that always throws is declared to return Nothing, and the compiler accepts it in
any branch because Nothing <: Int, Nothing <: String, and so on. ??? has type Nothing, and so do throw expressions and
sys.exit. Nothing is also what the compiler infers for a collection with no elements, and since List is covariant,
List[Nothing] conforms to List[String].
Null is the type of the literal null. It is a subtype of every reference type (every AnyRef type), so a String can be
null, and it is not a subtype of AnyVal types: an Int can never be null.
//> using scala 3.10.0
def fail(message: String): Nothing = throw IllegalStateException(message)
def grade(text: String): Int =
text.toIntOption match
case Some(g) => g // Int
case None => fail("no grade") // Nothing, so the match is still an Int
@main def nothingAndNull(): Unit =
println(grade("5"))
println(try grade("x") catch case e: IllegalStateException => e.getMessage)
val empty = List() // List[Nothing]
val trails: List[String] = empty // List is covariant, so List[Nothing] <: List[String]
println(trails.size)
println(Option.empty.getOrElse("none"))
val hut: String = null // Null <: String, allowed without explicit nulls
println(hut == null)
println(s"hut = $hut")
// val grades: Int = null // does not compile: Int is an AnyVal
val unknown = null // inferred as Null
println(unknown)
5
no grade
0
none
true
hut = null
null
The commented-out line is a compile-time error:
[error] Found: Null
[error] Required: Int
[error] Note that implicit conversions were not tried because the result of an implicit conversion
[error] must be more specific than Int
[error] val grade: Int = null
[error] ^^^^
A val whose right-hand side is only null or List() and which has no type annotation is inferred as Null or
List[Nothing], which is rarely what you meant. Annotate the declared type, for example val hut: String | Null = null.
Null and explicit nulls
Scala 3 offers an opt-in mode that makes null honest. With -Yexplicit-nulls, Null is no longer a subtype of String or any
other reference type (it extends AnyVal, although after erasure it is still a subtype of all reference types, as the JVM
requires), so a nullable value has to be written as the union String | Null. Java APIs, whose return types the compiler cannot know to be non-null, are given a "flexible" type that is
accepted either way, and a Scala method that returns a nullable must be unwrapped explicitly (.nn asserts non-null).
//> using scala 3.10.0
//> using option -Yexplicit-nulls
@main def explicit(): Unit =
val hut: String = null
println(hut)
[error] ./e/Explicit.scala:5:21
[error] Found: Null
[error] Required: String
[error] Note that implicit conversions were not tried because the result of an implicit conversion
[error] must be more specific than String
[error] val hut: String = null
[error] ^^^^
Annotating the type as a union compiles and runs; a Java call such as System.getProperty is flexible and is accepted as a
String or as a String | Null:
//> using scala 3.10.0
//> using option -Yexplicit-nulls
@main def ok(): Unit =
val hut: String | Null = null
println(hut == null)
val name = System.getProperty("no.such.property") // Java API: String | Null is flexible
println(name)
true
null
-Yexplicit-nulls is still an experimental, -Y option in 3.10, and libraries compiled without it are seen through the
flexible-type rule. Nulls and Option are compared on
Option & Null Safety; Kotlin builds nullability into the
type system by default (see Kotlin Null Safety), and Java has no
compile-time nullness.
Casting
Scala has two operations for working with the runtime class of a value: isInstanceOf[T] tests it and asInstanceOf[T]
converts it, throwing ClassCastException if the value is not a T. They are ordinary methods of Any, not keywords. Both
operate on the erased runtime class, which matters for generic types (see Erasure and unchecked casts).
//> using scala 3.10.0
@main def casting(): Unit =
val x: Any = "Refugio"
println(x.isInstanceOf[String])
println(x.isInstanceOf[Int])
val s = x.asInstanceOf[String]
println(s.length)
try
val n = x.asInstanceOf[Int]
println(n)
catch case e: ClassCastException => println(e.getClass.getSimpleName)
// Primitive conversions are methods, not casts
val d = 8.5
println(d.toInt)
println(65.toChar)
println((300: Int).toByte)
// Preferred: pattern matching, which tests and casts in one step
x match
case name: String => println(s"name of ${name.length} letters")
case _ => println("something else")
// Erased cast: unchecked, no exception at the cast
val ys: List[Any] = List("a", "b")
val ints = ys.asInstanceOf[List[Int]]
println(ints.size)
try println(ints.head + 1) catch case e: ClassCastException => println("ClassCastException at use")
true
false
7
ClassCastException
8
A
44
name of 7 letters
2
ClassCastException at use
Numeric conversions between value types are done with toInt, toChar, toByte and the like, not with asInstanceOf, and
they truncate or wrap silently (300.toByte is 44). A type test pattern such as case name: String is both the test and the
cast, with the cast proven safe by the test, so prefer it to the isInstanceOf/asInstanceOf pair. The cast to List[Int]
did not fail: asInstanceOf can only check the erased class List, and the failure arrived later, at the + 1 that
treated an element as an Int.
Matchable
Matchable is a marker trait: it has no members and is a subtype of Any. A value can be used as the scrutinee of a pattern
match, or in a type test, only if its static type is a subtype of Matchable. Almost every type qualifies. AnyVal and AnyRef
extend Matchable, so every class, every Int, every String, a Trail and a Tuple are Matchable. The exceptions are
Any itself and the type parameters and abstract types that are only known to be Any.
Scala 3.10 compiles the following code silently. With -source:future, which switches on checks planned for a later
release, the compiler warns that the pattern selector is not known to be Matchable.
//> using scala 3.10.0
//> using option -source:future
def describe(x: Any): String =
x match
case s: String => s"string ${s.length}"
case n: Int => s"int $n"
case _ => "other"
def describe2(x: Matchable): String =
x match
case s: String => s"string ${s.length}"
case _ => "other"
@main def m(): Unit =
println(describe("a"))
println(describe2("abc"))
[warn] ./g1/M.scala:6:13
[warn] pattern selector should be an instance of Matchable,
[warn] but it has unmatchable type Any instead
[warn] case s: String => s"string ${s.length}"
[warn] ^^^^^^
[warn] ./g1/M.scala:7:13
[warn] pattern selector should be an instance of Matchable,
[warn] but it has unmatchable type Any instead
[warn] case n: Int => s"int $n"
[warn] ^^^
string 1
string 3
Only describe is flagged; describe2 takes a Matchable and compiles cleanly. In 3.10 these checks are still behind
-source:future; without that option the program above compiles with no warning at all, as it did in Scala 2.
Why Matchable exists
The reason is parametricity, and the example is an opaque type. Inside the scope that defines it, opaque type Km = Double is
a Double; outside, it is an abstract type about which nothing is known. A generic function def f[T](x: T) is supposed to be
unable to inspect x, so clients get a free theorem about it. But a pattern match on x is exactly such an inspection: it
looks through the abstraction at the underlying representation.
//> using scala 3.10.0
//> using option -source:future
object Units:
opaque type Km = Double
object Km:
def apply(value: Double): Km = value
extension (km: Km) def value: Double = km
import Units.*
def leak[T](x: T): String =
x match
case d: Double => s"it is really a Double: $d"
case _ => "opaque"
@main def opaque(): Unit =
println(leak(Km(8.5)))
[warn] ./g2/Opaque.scala:14:13
[warn] pattern selector should be an instance of Matchable,
[warn] but it has unmatchable type T instead
[warn] case d: Double => s"it is really a Double: $d"
[warn] ^^^^^^
it is really a Double: 8.5
Km was meant to hide the fact that it is a Double, yet the generic function found out, because at run time an opaque type
is its representation. The warning is what Scala 3 added to close this hole: T is not known to be Matchable, so matching on
it is flagged. A function that really wants to inspect its argument has to say so by bounding T <: Matchable, and the
compiler then checks the bound at each call site. An opaque type whose upper bound is not Matchable does not meet it:
//> using scala 3.10.0
//> using option -source:future
object Units:
opaque type Km = Double
object Km:
def apply(value: Double): Km = value
import Units.*
def check[T <: Matchable](x: T): String =
x match
case _: Double => "Double"
case _ => "other"
def bad(x: Km): String = check(x)
@main def p(): Unit = println(bad(Km(1)))
[warn] ./p/P.scala:16:32
[warn] value should be an instance of Matchable,
[warn] but it has unmatchable type Units.Km instead
[warn] def bad(x: Km): String = check(x)
[warn] ^
Double
The call is flagged but, like the other checks, still compiles under 3.10, and the match then sees the Double. So the
warnings describe a rule that the language will enforce later; today they are a lint that you switch on with
-source:future to find the places where code looks through an abstraction. The safe habit is to treat an Any or an
unconstrained T as opaque, and to bound with Matchable only the functions whose job is to inspect values.
To get a Matchable from an Any you cannot change, for example a value returned by a library, call asMatchable, an
extension method of scala.compiletime. It is a deliberate, visible cast that is allowed because the value really is an
object, a boxed number or an array at run time.
//> using scala 3.10.0
//> using option -source:future
import scala.compiletime.asMatchable
def show(x: Any): String =
x.asMatchable match
case s: String => s"string $s"
case n: Int => s"int $n"
case _ => "other"
@main def asMatch(): Unit =
println(show("a"))
println(show(3))
println(show(2.5))
string a
int 3
other
Without the import, x.asMatchable fails with "value asMatchable is not a member of Any, but could be made available as an
extension method", and the compiler’s suggested import is scala.compiletime.asMatchable.
Erasure and unchecked casts
Generic type arguments are erased by the JVM, so at run time List[Int] and List[String] are the same class. A type test
cannot distinguish them, and the compiler says so instead of letting the test silently succeed.
//> using scala 3.10.0
def kind(xs: Any): String =
xs match
case _: List[Int] => "list of Int?"
case _ => "other"
@main def erasure(): Unit =
println(kind(List("a", "b")))
[warn] ./i/Erasure.scala:5:10
[warn] the type test for List[Int] cannot be checked at runtime because its type arguments can't be determined from Any
[warn] case _: List[Int] => "list of Int?"
[warn] ^
list of Int?
The program printed list of Int? for a list of strings. The warning is the compiler’s way of saying that only List was
tested. The type argument can only be tested when it follows from the scrutinee’s static type: a List[Int] scrutinee can be
matched against ::[Int] without a warning, because the element type is already known, whereas an Any scrutinee gives
the test nothing to derive it from. The cast asInstanceOf[List[Int]] shown earlier is not checked either. The type of an array, in contrast, is kept at run time: Array[Int] is an int[] on the JVM, which is why creating
one from a generic T requires a ClassTag.
TypeTest and ClassTag
Two type classes let a generic function perform a type test that the erased T could not. ClassTag[T] carries the runtime
class of T, and the compiler supplies it automatically for any concrete class type. A type test against an abstract T with
a ClassTag in scope is then allowed, and so is the creation of an Array[T]. TypeTest[S, T] is the more general mechanism
introduced in Scala 3: a given instance that knows how to decide whether a value of type S is a T, for any T, including
a type that has no runtime class, such as an opaque type. It is used by the compiler whenever a pattern case b: B tests
against an abstract type B.
//> using scala 3.10.0
import scala.reflect.{ClassTag, TypeTest}
// ClassTag: a runtime class, enough for a type test on a class type and for arrays
def firstOf[T](xs: List[Any])(using ClassTag[T]): Option[T] =
xs.collectFirst { case t: T => t }
// TypeTest: a type test for a type the compiler cannot check by itself
def narrow[A, B <: A](a: A)(using TypeTest[A, B]): Option[B] =
a match
case b: B => Some(b)
case _ => None
object Units:
opaque type Km = Double
object Km:
def apply(value: Double): Km = value
extension (km: Km) def value: Double = km
// an opaque type has no runtime class, so its author supplies the test
given TypeTest[Any, Km] with
def unapply(x: Any): Option[x.type & Km] =
x match
case d: Double => Some(d.asInstanceOf[x.type & Km])
case _ => None
import Units.*
@main def typeTests(): Unit =
val items = List(1, "pico", 2.5, "refugio")
println(firstOf[String](items))
println(firstOf[Double](items))
println(narrow[Any, String]("pico"))
println(narrow[Any, String](3))
println(narrow[Any, Km](8.5).map(_.value))
println(narrow[Any, Km]("8.5").map(_.value))
println(Array.ofDim[Int](2).getClass.getSimpleName)
Some(pico)
Some(2.5)
Some(pico)
None
Some(8.5)
None
int[]
For the opaque Km, the given in Units is the only place that can safely say what "being a Km`" means at run time, and a
caller outside `Units gets a working case k: Km without learning that Km is a Double. The library also provides
TypeTest instances automatically for class types whose test the compiler can derive. A ClassTag is not a complete
substitute for the type: it records only the erased class, so the tag of List[Int] and the tag of List[String] are equal.
//> using scala 3.10.0
import scala.reflect.ClassTag
@main def classTags(): Unit =
println(summon[ClassTag[List[Int]]] == summon[ClassTag[List[String]]])
println(summon[ClassTag[List[Int]]].runtimeClass.getName)
true
scala.collection.immutable.List
The ClassTag context bound is the Scala 3 spelling of what Java does with Class<T> tokens (see
Java Generics); reified type parameters in Kotlin inline functions solve the
same problem at compile time. Context bounds are covered on
Context Bounds & Deferred Givens.
Product and tuples as products
scala.Product is the trait of things made of a fixed number of elements. It declares productArity, productElement(n),
productIterator, productPrefix and productElementNames, and it extends Equals. Every case class extends Product
automatically, the compiler generating the implementations, and so does every enum case that is a case class and every
Tuple. The trait is how generic code such as a pretty-printer or a serializer reads the fields of any case class without
reflection on its names.
//> using scala 3.10.0
enum Difficulty:
case Easy, Moderate, Hard
case Alpine(grade: Int)
case class Trail(name: String, lengthKm: Double, ascentM: Int, difficulty: Difficulty)
@main def products(): Unit =
val pico = Trail("Pico", 8.5, 900, Difficulty.Hard)
println(pico.productArity)
println(pico.productPrefix)
println(pico.productElement(1))
println(pico.productElementNames.toList)
println(pico.productIterator.toList)
val t = ("Pico", 8.5, 900)
println(t.isInstanceOf[Product])
println(t.isInstanceOf[Tuple3[?, ?, ?]])
println(t.getClass.getName)
println(t.productArity)
println(t.toList) // Tuple.toList: List[String | Double | Int]
println(Tuple.fromProduct(pico)) // case class to tuple, via Mirror
println(EmptyTuple.getClass.getSimpleName)
4
Trail
8.5
List(name, lengthKm, ascentM, difficulty)
List(Pico, 8.5, 900, Hard)
true
true
scala.Tuple3
3
List(Pico, 8.5, 900)
(Pico,8.5,900,Hard)
EmptyTuple$
A tuple type (String, Double, Int) is a Tuple3[String, Double, Int], which is a Product3, which is a Product; in Scala 3
the tuple is also a Tuple, the generic cons-style type (String *: Double *: Int *: EmptyTuple) that supports operations
over tuples of any size. The elements are read with t(0) (typed) or productElement(0) (typed Any). Tuples of
up to 22 elements are scala.TupleN classes, and larger ones are backed by scala.runtime.TupleXXL (a 23-element tuple has
productArity 23 and that class), a size Scala 2 did not allow. productElement loses the type; use it for generic code and prefer pattern matching or the typed accessors elsewhere.
Tuples, named tuples and NamedTuple.From are on
Tuples & Named Tuples.
Differences from Scala 2, Java and Kotlin
-
Scala 2 has no
Matchable:Anyis directly the parent ofAnyValandAnyRef, and every value can be matched on. In Scala 3 the trait is inserted between them, but the checks are off unless-source:futureis given, so code that compiles with 2.13 still compiles. -
Scala 2 has
ClassTagbut noTypeTest, which is new in Scala 3. -
Java separates primitives (
int,double) from objects and hasvoidas a pseudo-type; Scala folds both into one hierarchy, withUnitas a real type whose value is(). A Java method returningvoidis seen as returningUnit. -
Java’s
Objectcorresponds toAnyRef;Anyhas no Java counterpart and is erased toObject. -
Kotlin has the same shape with different names:
Anyis the root of non-null types (Scala’sAnyRef),Any?the root of all types,Unitis a real type,Nothingis the bottom type, andNothing?is the type ofnull. Kotlin’sasandiscorrespond toasInstanceOfandisInstanceOf, andas?has no direct Scala equivalent; the idiomatic Scala spelling is a type pattern in a match. -
-Yexplicit-nullsbrings Scala’s nullability close to Kotlin’s, withT | Nullplaying the role ofT?.
Where the books are outdated
The Scala 3 Book and the Tour of Scala describe the hierarchy accurately, but check these points against the current documentation:
-
Programming in Scala and other Scala 2 books show
Anydirectly aboveAnyValandAnyRef. They do not mentionMatchable,TypeTestor the-source:futurewarnings. Their advice to match on anAnyis still valid in 3.10, with no warning unless you opt in. -
Scala 2 books show
ClassTagandManifest. Scala 3.10 still synthesizes aManifestfor compatibility, but that synthesis is deprecated and the warning tells you to useClassTaginstead. -
The books describe
nullas always available for reference types. With-Yexplicit-nullsthat is no longer true. -
Version and JDK baselines: Scala 3.10 needs JDK 17 or later.