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.

classDiagram Any <|-- Matchable Matchable <|-- AnyVal Matchable <|-- AnyRef AnyVal <|-- Int AnyVal <|-- Long AnyVal <|-- Double AnyVal <|-- Boolean AnyVal <|-- Char AnyVal <|-- Unit AnyRef <|-- String AnyRef <|-- Product AnyRef <|-- Option Product <|-- Trail AnyRef <|-- Null Null <|-- Nothing Int <|-- Nothing note for Any "equals, hashCode, toString, ==, ##, getClass, isInstanceOf, asInstanceOf" note for AnyRef "adds eq, ne, synchronized, wait, notify" note for Nothing "no values, subtype of every type" note for Null "only value is null, 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.

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: Any is directly the parent of AnyVal and AnyRef, and every value can be matched on. In Scala 3 the trait is inserted between them, but the checks are off unless -source:future is given, so code that compiles with 2.13 still compiles.

  • Scala 2 has ClassTag but no TypeTest, which is new in Scala 3.

  • Java separates primitives (int, double) from objects and has void as a pseudo-type; Scala folds both into one hierarchy, with Unit as a real type whose value is (). A Java method returning void is seen as returning Unit.

  • Java’s Object corresponds to AnyRef; Any has no Java counterpart and is erased to Object.

  • Kotlin has the same shape with different names: Any is the root of non-null types (Scala’s AnyRef), Any? the root of all types, Unit is a real type, Nothing is the bottom type, and Nothing? is the type of null. Kotlin’s as and is correspond to asInstanceOf and isInstanceOf, and as? has no direct Scala equivalent; the idiomatic Scala spelling is a type pattern in a match.

  • -Yexplicit-nulls brings Scala’s nullability close to Kotlin’s, with T | Null playing the role of T?.

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 Any directly above AnyVal and AnyRef. They do not mention Matchable, TypeTest or the -source:future warnings. Their advice to match on an Any is still valid in 3.10, with no warning unless you opt in.

  • Scala 2 books show ClassTag and Manifest. Scala 3.10 still synthesizes a Manifest for compatibility, but that synthesis is deprecated and the warning tells you to use ClassTag instead.

  • The books describe null as always available for reference types. With -Yexplicit-nulls that is no longer true.

  • Version and JDK baselines: Scala 3.10 needs JDK 17 or later.