Generics, variance and type bounds

Parameterise types, set upper and lower bounds, and get variance right so a subtype relationship survives inside a container.

Type parameters and bounds

// an unbounded parameter: any type works
def first[A](xs: List[A]): Option[A] = xs.headOption

// an upper bound: A must be comparable to itself
def maxOf[A <: Comparable[A]](xs: List[A]): Option[A] =
  xs.reduceOption((a, b) => if a.compareTo(b) >= 0 then a else b)

// a context bound: an Ordering[A] must be available
def sorted[A: Ordering](xs: List[A]): List[A] = xs.sorted

// equivalent, spelled out
def sorted2[A](xs: List[A])(using ord: Ordering[A]): List[A] = xs.sorted

// a lower bound: the result type must be a supertype of both
def cons[A >: Nothing](head: A, tail: List[A]): List[A] = head :: tail

// a type member instead of a parameter, for a companion-style API
trait Repository:
  type Id
  def find(id: Id): Option[String]

class UserRepository extends Repository:
  type Id = java.util.UUID
  def find(id: Id): Option[String] = None
SyntaxNameMeaning
[A]Type parameterAny type, including Nothing
[A <: Bound]Upper boundA must be a subtype of Bound
[A >: Bound]Lower boundA must be a supertype of Bound
[A: TC]Context boundA TC[A] instance must be available
[A: TC as a]Named context boundScala 3: name the instance for reuse
[A & B]IntersectionA with both capabilities
[A | B]UnionA or B

Variance

Variance answers one question: if a Dog is an Animal, is a List[Dog] a List[Animal]? Scala makes you answer it in the declaration, and the compiler checks that your answer is safe.

class Animal
class Dog extends Animal
class Puppy extends Dog

// covariant: a producer. List[+A] means List[Dog] IS a List[Animal]
sealed trait Producer[+A]:
  def get: A

// contravariant: a consumer. Consumer[Animal] IS a Consumer[Dog]
sealed trait Consumer[-A]:
  def accept(a: A): Unit

// invariant: both roles, so no subtyping either way
class Box[A](var value: A)

val p: Producer[Animal] = new Producer[Dog] { def get = new Dog }
val c: Consumer[Dog]    = new Consumer[Animal] { def accept(a: Animal) = () }

// this is what makes variance rules: a covariant parameter may only appear
// in an output position, otherwise the compiler rejects the declaration
// class Bad[+A](var value: A)   // error: covariant type A occurs in a contravariant position

def printAll[A](xs: List[A]): Unit = xs.foreach(println)
printAll(List(new Dog, new Puppy))       // List[Puppy] works where List[Dog] is expected

// a wildcard: accept any element type
def size(xs: List[?]): Int = xs.size
def total(xs: List[? <: Number]): Double = xs.map(_.doubleValue).sum
  • Read the question as "who supplies and who consumes". A value you only return can be covariant; a value you only accept can be contravariant.
  • Function1[-T, +R] is contravariant in the argument and covariant in the result, which is exactly the intuition above.
  • Mutable state forces invariance. Array is invariant because a write would otherwise be unsound.
  • ? is Scala 3's wildcard for an existential type; _ still works but is on the way out.

Type lambdas and self-referential types

// a self-referential F-bounded type: compare with the concrete subtype
trait Comparable[A <: Comparable[A]]:
  def compareTo(other: A): Int

final case class Version(major: Int, minor: Int) extends Comparable[Version]:
  def compareTo(other: Version): Int =
    (major, minor).compareTo((other.major, other.minor))

// a type lambda: fix one parameter of a two-parameter type
type IntMap[V] = Map[Int, V]                    // Scala 3 alias, the common case
type EitherString[A] = Either[String, A]

def lookup[F[_], A](fa: F[A]): F[A] = fa        // F is a type constructor
val r: EitherString[Int] = lookup[EitherString, Int](Right(1))

// match types let the compiler compute a type from another type
type Elem[X] = X match
  case String      => Char
  case Array[t]    => t
  case List[t]     => t

val c: Elem[String] = 'a'
val i: Elem[List[Int]] = 1
💡
Variance is a declaration-site property in Scala, unlike Java's use-site wildcards. If the compiler rejects your variance annotation, it has found a real hole: writing through a covariant reference, or reading through a contravariant one, would break type safety at run time.

FAQ

Why does the compiler say my covariant type occurs in a contravariant position?
A parameter of a method is an input, so a covariant A cannot appear there. Either make the member private or protected, or add a lower bound such as def add[B >: A](b: B), which is the standard workaround.
What does Nothing mean?
Nothing is a subtype of every type and has no values. It types expressions that never return, like throw, which is why Left in a comprehension or Nil for a list fits anywhere.

Givens, implicits and type classes Traits, classes and object-oriented Scala

Last refreshed 2026-09-18.