An existential type says: there is some type here, I just will not tell you which. List[?] is a list of something. Array[?] is an array of something. You can ask such a value for its length, iterate it, or hand it to a generic method, but you cannot put an arbitrary value into it, because the element type is hidden and the compiler refuses to guess.

Most Scala developers meet existentials through Java interop, error messages that mention _$1, or a migration to Scala 3 that rejects forSome. This article explains how the compiler reasons about a hidden type, what Scala 3 removed, and the patterns that replace it, ending with a type-safe event-handler registry.

Advertisement

The problem: invariance leaves no common supertype

Start with a mutable container. Array[Int] is invariant in its element type, so it is not an Array[Any]. That rule is what keeps arrays sound: if the assignment were allowed, you could write a String into an array the rest of the program believes holds integers.

Invariance has a cost. Suppose you want a function that prints the length of any array, or a list holding arrays of different element types. Array[Any] does not work, because nothing except an Array[Any] conforms to it. You need a type that means an array of some element type, unknown to me. That is an existential type, and in current Scala you write it with a wildcard:

val ints: Array[Int] = Array(1, 2, 3)
// val anys: Array[Any] = ints        // does not compile: Array is invariant
val some: Array[?] = ints             // Scala 3 (Scala 2: Array[_])

def length(xs: Array[?]): Int = xs.length
val mixed: List[Array[?]] = List(Array(1, 2), Array("a", "b", "c"))
mixed.map(length)                     // List(2, 3)

Every Array[T] conforms to Array[?], whatever T is. In return, the operations available on the wildcard type are only those that are safe for every possible element type. Reading gives you a value of the upper bound, which is Any here. Writing needs a value of the lower bound, which is Nothing, so in practice you cannot write at all.

Scala 2's two syntaxes: forSome and the wildcard shorthand

Scala 2 had a general existential form, T forSome { type X; ... }, and the wildcard _ as shorthand for its common case. List[_] meant List[X] forSome { type X }. Bounds carried over: List[_ <: Number] meant List[X] forSome { type X <: Number }. The general form could say things wildcards cannot, because one hidden type could appear in several positions:

// Scala 2 only
import scala.language.existentials

type AnyList   = List[X] forSome { type X }                  // same as List[_]
type Paired    = (List[X], X => String) forSome { type X }   // one hidden X used twice
type Rows      = Map[Class[X], X] forSome { type X }          // NOT a heterogeneous map

Paired is the interesting one: a list together with a function that can render that list's elements. A wildcard version, (List[_], _ => String), hides two independent types and loses the link. Rows is a classic trap: it reads like a map where each key's class matches its value, but it actually means a map in which one hidden X is shared by every entry. It is not a heterogeneous map at all.

Since Scala 2.10, existentials that cannot be written with wildcards trigger a feature warning unless you import scala.language.existentials.

You wantScala 2Scala 3
Any element typeList[_]List[?] (_ still accepted, being phased out)
Upper-boundedList[_ <: Number]List[? <: Number]
Lower-boundedList[_ >: Int]List[? >: Int]
One hidden type used in two places(List[X], X => String) forSome { type X }Not expressible as a type; use a packed class or type member
Java raw type ClassClass[_]Class[?]
Advertisement

How the compiler opens an existential: skolems

Packing hides a type; opening it gives you a fresh, unnamed type you can use but not escapeArray[Int]concrete, invariantArray[?]element type hiddenArray[Any]NOT a supertypepack (widen)noopen: skolem Afresh type, e.g. _$1use at a call sitedef f[A](xs: Array[A])capture conversion binds Axs.length, xs.toListoperations that ignore Axs(0) = valuerejected: A is unknownScala 2 wrote the general form as T forSome { type T }; Scala 3 keeps only wildcards (?, formerly _)and moves everything else to type parameters, type members and packed case classes.
Packing widens a concrete type to a wildcard; using the value opens it to a fresh skolem type that the compiler tracks but you cannot name.

When you use a value of type Array[?], the compiler does not treat the element type as Any. It invents a fresh, unnamed type, called a skolem, that stands for the specific hidden type of this particular value. Scala 2 prints skolems as _$1, _$2 and so on. Knowing this explains the most confusing error messages in the area:

def swapFirstTwo(xs: Array[?]): Unit =
  val tmp = xs(0)        // tmp: Any (the skolem widened to its bound)
  xs(0) = xs(1)          // error: the element type of xs is unknown
  xs(1) = tmp            // error: Any is not the hidden element type

Both writes fail because the compiler cannot prove that xs(1) has the same skolem as the slot it is assigned to; reading from the wildcard has already widened it to Any. The fix is to give the hidden type a name by calling a generic method. When you pass an Array[?] to a method expecting Array[A], the compiler binds A to the skolem. This is called capture conversion, and both Scala 2 and Scala 3 perform it:

def swapFirstTwo(xs: Array[?]): Unit = swapTyped(xs)   // A := the hidden type

private def swapTyped[A](xs: Array[A]): Unit =
  val tmp: A = xs(0)
  xs(0) = xs(1)
  xs(1) = tmp

Inside swapTyped the type has a name, so reads and writes line up. This one technique, a public method taking a wildcard and a private generic helper that does the work, resolves most real-world existential errors.

What Scala 3 dropped, and why

Scala 3 removed forSome. The Scala 3 reference lists it under dropped features: general existential types do not fit the DOT calculus that Scala 3's type system is built on, and they interacted badly with type inference and path-dependent types. What remains is the wildcard form, which Scala 3 models as a bounded type argument rather than as a separate kind of type.

Scala 3 also changed the spelling. ? is the wildcard. _ is still accepted for compatibility, but its role as a wildcard is being phased out so that _ can later mean a type lambda placeholder. Two practical consequences follow. First, if you used the kind-projector compiler plugin, its old ? placeholder clashes with Scala 3's wildcard; kind-projector switched to * for that reason, and Scala 3 offers a -Ykind-projector option for cross-built code. Second, Scala 2.13 accepts ? as a wildcard under the -Xsource:3 flag, so cross-built code can use one spelling in both.

Every other use of forSome falls into one of three shapes, each with a direct replacement.

Replacement 1: a type parameter on the method

If the hidden type only matters inside one method, make it a type parameter. def f(x: (List[X], X => String) forSome { type X }) becomes def f[X](xs: List[X], render: X => String). Callers already know the type, so they lose nothing.

Replacement 2: a type member, opened by path

If the value must travel, stored in a collection or returned from a function, you need a value that carries its type with it. A trait with an abstract type member does exactly that, and Scala's path-dependent types let the compiler connect a value to its own hidden type:

trait Show[A]:
  def show(a: A): String

trait Packed:
  type T
  def value: T
  def shower: Show[T]

def pack[A](a: A)(using s: Show[A]): Packed = new Packed:
  type T = A
  val value  = a
  val shower = s

given Show[Int]    = i => s"Int($i)"
given Show[String] = s => s"'$s'"

val bag: List[Packed] = List(pack(42), pack("hi"))
bag.map(p => p.shower.show(p.value))   // List(Int(42), 'hi')

Inside the lambda, p.value has type p.T and p.shower accepts p.T, so the call type-checks even though nobody knows what p.T is. This is exactly the Paired existential from earlier, expressed with a type member instead of forSome. The path-dependent types article covers the mechanism in more depth.

Replacement 3: a packed case class with a pattern-bound type

The lightest replacement is an ordinary generic class whose type parameter you hide with a wildcard when storing it, then recover when you use it:

final case class Shown[A](value: A, ev: Show[A]):
  def render: String = ev.show(value)          // typed inside the class

val items: List[Shown[?]] = List(Shown(42, summon[Show[Int]]), Shown("hi", summon[Show[String]]))
items.map(_.render)

items.map { case s: Shown[a] => s.ev.show(s.value) }   // a is bound to the hidden type

Two things make this work. Methods defined inside Shown see A as a real type, so render is always well typed. And a lowercase type variable in a pattern, Shown[a], binds the hidden type for the duration of the case, the pattern-matching equivalent of capture conversion. No cast is involved.

Worked example: a type-safe event-handler registry

A common real need is a registry that maps event classes to handlers. The naive version stores Map[Class[?], Any => Unit] and casts on every dispatch, which moves type errors from compile time to production. Using the packed-class pattern and a private generic helper gives a version where each handler is typed against its own event class:

import scala.reflect.ClassTag

final case class Handler[E](cls: Class[E], run: E => Unit)

final class Registry:
  private var handlers: List[Handler[?]] = Nil

  def on[E <: AnyRef](run: E => Unit)(using ct: ClassTag[E]): Unit =
    handlers ::= Handler(ct.runtimeClass.asInstanceOf[Class[E]], run)

  /** Returns how many handlers accepted the event. */
  def dispatch(event: AnyRef): Int =
    handlers.count(h => fire(h, event))   // h: Handler[?] is captured as Handler[E]

  private def fire[E](h: Handler[E], event: AnyRef): Boolean =
    if h.cls.isInstance(event) then
      h.run(h.cls.cast(event))              // typed: cast returns E, run takes E
      true
    else false

final case class OrderPlaced(id: String, cents: Long)
final case class OrderCancelled(id: String)

val r = Registry()
r.on[OrderPlaced](e => println(s"placed ${e.id}"))
r.dispatch(OrderPlaced("A-17", 4_250))   // prints "placed A-17", returns 1

Follow the types through one dispatch. handlers is a List[Handler[?]], a list of handlers for some event type each. handlers.count(h => fire(h, event)) passes each Handler[?] to fire[E], and capture conversion binds E to that handler's hidden type. Inside fire, h.cls.cast(event) returns an E and h.run accepts an E, so the compiler checks the link that the naive map could only assume.

Note the E <: AnyRef bound on on. Without it, someone can register on[Int]. The ClassTag[Int] has the primitive class int as its runtime class, and classOf[Int].isInstance(boxedInteger) is false, so the handler silently never fires. The bound turns that run-time surprise into a compile error. The same reasoning is why ClassTag cannot distinguish List[Int] from List[String]: erasure removes type arguments, so key events on concrete case classes, not on generic types.

Java interop: wildcards, raw types and Class

Java's use-site variance maps directly onto Scala wildcards. A Java signature List<? extends Number> appears in Scala as java.util.List[? <: Number]. Raw Java types, such as a legacy API returning plain Class or List, are seen as Class[?] and java.util.List[?].

Failure modes

  • Reaching for Any instead of ?. List[Array[Any]] rejects every Array[Int] because of invariance; the wildcard is the type you meant.
  • Expecting a heterogeneous map from Map[Class[?], ?]. Two wildcards are two unrelated hidden types. The key's class and the value's type are not linked; use a packed entry class or a typed key.
  • Leaking a skolem. Returning a value whose type mentions a skolem from a local scope produces errors naming _$1 or similar. Give the type a name with a type parameter and return it through that.
  • Writing through a wildcard. Updates through Array[?] or mutable.Buffer[?] fail by design. Route them through a generic helper that names the type.
  • Migration by deletion. Removing forSome and putting ? in each position compiles in some cases but quietly breaks a link between positions. Check every rewrite that had one variable in two places.

Trade-offs

Wildcards are cheap and readable, and they are the right tool whenever the code genuinely does not care about the hidden type. Type parameters are the most precise option but push the type onto callers, which is fine for functions and awkward for values stored in collections. Type members carry types with values, at the cost of path-dependent types that some readers find hard to follow. Packed case classes sit in the middle: explicit, pattern-matchable and easy to explain in a code review.

Related techniques solve neighbouring problems. Opaque types hide a representation behind a name rather than hiding an unknown type, and given instances and implicits are how the packed evidence, such as Show[A] above, is found. For the wider picture of variance, bounds and inference, see the Scala type system overview, and for everything else that changed in the language, the Scala 3 guide.

What to do next

  1. Search your codebase for forSome and scala.language.existentials; each hit is a Scala 3 migration task.
  2. Classify every hit: used in one method (type parameter), stored and later used with its evidence (packed case class or type member), or wildcard-expressible (replace with ?).
  3. For each rewrite where one hidden type appeared in two positions, add a test proving the link still holds, such as the Paired list being renderable by its own function.
  4. Adopt ? as the wildcard spelling now; on Scala 2.13 enable -Xsource:3 so cross-built code uses one form.
  5. Replace any Map[Class[?], Any => Unit]-style registry with the packed-handler pattern and bound its type parameter to AnyRef.
  6. When an error message names _$1 or a similar skolem, apply the public-wildcard, private-generic-helper split before trying anything else.
Key takeaway: An existential type is a type with a hidden component: some element type, unknown at this point in the program. Wildcards such as List[?] are the everyday form, needed because invariant types like Array[Int] have no useful common supertype. The compiler opens a wildcard to a fresh skolem type, which is why reads widen to the bound and writes fail; naming the type through a generic helper, via capture conversion, fixes most errors. Scala 3 dropped the general forSome form, so express a hidden type used in several positions with a method type parameter, a trait with a type member, or a packed case class recovered by a pattern-bound type variable.