Most Scala types are nominal: a value has type Closeable because its class says extends Closeable. A structural type instead describes a shape. { def close(): Unit } is satisfied by any object that has a public close method with that signature, whatever its class declares. That sounds like duck typing, and it is, but with one important difference: the compiler still checks every call against the declared shape. Only the dispatch is dynamic.

That combination makes structural types useful in a few narrow places (adapting third-party classes you cannot change, typed views over dynamic data) and a trap almost everywhere else. This page explains what the compiler actually generates in Scala 2 and Scala 3, the rules and limits the Scala 3 reference documents, what a structural call costs, and how to move off them when they turn up in a hot path.

Advertisement

Refinement types: the general mechanism

A structural type is a special case of a refinement type: a parent type followed by a block of extra member declarations. Animal { def speak(): String } means "an Animal that also has a speak method". When you write only the block, the parent is AnyRef, so { def close(): Unit } is shorthand for AnyRef { def close(): Unit }.

Refinements come in two flavours, and only one of them is expensive. A refinement that fixes a type member, such as Container { type Elem = Int }, is pure type-level information. It costs nothing at runtime, because the member already exists in the parent and the refinement only narrows what the compiler knows. The same is true when a refinement narrows a val or def the parent already declares. The expensive case is a refinement that adds a term member the parent does not have. The JVM has no instruction for "call a method named close on whatever this is", so the compiler has to route the call through something else.

trait Container:
  type Elem
  def head: Elem

// Type-member refinement: static only, ordinary virtual calls.
def first(c: Container { type Elem = Int }): Int = c.head + 1

// Term-member refinement: close is not a member of AnyRef,
// so this call needs a dispatch mechanism (see below).
type Closeable = { def close(): Unit }

Type-member refinements are the bread and butter of path-dependent APIs, covered in Scala path-dependent types. The rest of this page is about the term-member case.

Scala 2: reflective calls behind a feature flag

In Scala 2, a call to a refinement member compiles to Java reflection. The compiler emits code that looks up a java.lang.reflect.Method by name and erased parameter types on the receiver's runtime class, caches the lookup at the call site, boxes primitive arguments, invokes the method and casts the result back. Because this is slow and surprising, Scala 2.10 put it behind a language feature: without import scala.language.reflectiveCalls (or the -language:reflectiveCalls compiler flag) every such call produces a feature warning.

import scala.language.reflectiveCalls

def using[R <: { def close(): Unit }, A](r: R)(f: R => A): A =
  try f(r) finally r.close()   // reflective call in Scala 2

// Accidental structural type: the anonymous class's extra member
// is part of the inferred type, so p.port is a reflective call.
val p = new AnyRef { def port: Int = 8080 }
p.port

The second example is the more common source of reflective calls in old codebases: nobody wrote a structural type, but the compiler inferred one from an anonymous class. Annotating the val with a named trait removes it. Scala 2 also has a typing restriction worth knowing: a parameter type inside a structural refinement may not refer to an abstract type defined outside that refinement, because the erased signature needed for the reflective lookup would not be known.

Advertisement

Scala 3: Selectable and the rewrite rules

Scala 3 replaced hard-wired reflection with a library hook. Any refinement member access on a value whose type is (or converts to) a subtype of scala.Selectable is rewritten by the compiler into calls on that value. The Scala 3 reference states the rules:

  • A field access v.a of type U becomes v.selectDynamic("a").asInstanceOf[U].
  • A method call v.a(args) becomes v.applyDynamic("a")(args).asInstanceOf[R]. Multiple argument lists are flattened into one.
  • If the Selectable defines the second form, applyDynamic(name: String, ctags: Class[?]*)(args: Any*), the compiler also passes classOf of each declared parameter type, so an implementation can resolve overloads by erased signature.
What the Scala 3 compiler does with one structural callSourcer.close() where r: Closeable-ishTyperis close a member of the parent?Yes: ordinary callvirtual dispatch, no SelectableNo: refinement memberrewrite through SelectableRewriter.applyDynamic("close", [classOf args])(args).asInstanceOf[Unit]reflect.Selectable (reflectiveSelectable)Java reflection lookup + invokeYour own SelectableMap lookup, JSON node, DB row ...import the conversionextend SelectableType checking is static either way; only the dispatch is dynamic.A mismatch between the refinement and the runtime object fails at the call, not at compile time.
A call to a member the parent type already has is ordinary dispatch. A call to a refinement-only member is rewritten into selectDynamic or applyDynamic on a Selectable, which is either the reflective adapter or your own class.

If the receiver is not a Selectable, there is no hidden reflection. The call simply does not compile, which is a deliberate change: the reflective behaviour has to be opted into by name.

reflectiveSelectable: opting into reflection

The opt-in is an implicit conversion, scala.reflect.Selectable.reflectiveSelectable, which wraps any value in a reflect.Selectable whose selectDynamic and applyDynamic use Java reflection on the wrapped object's runtime class. The reference calls the import a visible warning about the cost, which is the right way to read it in code review.

import scala.reflect.Selectable.reflectiveSelectable

type Closeable = { def close(): Unit }

def closeQuietly(r: Closeable): Unit =
  try r.close()            // rewritten: applyDynamic("close", ...)()
  catch case scala.util.control.NonFatal(e) => log.warn(s"close failed: $e")

// Works for classes that share a method name but no interface:
class LegacyPool  { def close(): Unit = () }
class VendorSocket { def close(): Unit = () }
closeQuietly(new LegacyPool)
closeQuietly(new VendorSocket)

The compile-time check is real: passing an object with no close() is a type error. What is not checked is anything the compiler cannot see, such as a close that exists on the static type but is not public on the runtime class, or how an exception thrown by the target surfaces through the reflective invoke. Test the call path against every concrete class you expect, and catch failures at the boundary rather than letting a reflection exception escape from deep inside business logic.

Programmatic structural types: typed records

The more interesting Scala 3 use is implementing Selectable yourself. The reference's example is a record backed by a map, given a structural type so that field access is typed:

class Record(elems: (String, Any)*) extends Selectable:
  private val fields = elems.toMap
  def selectDynamic(name: String): Any = fields(name)

type Person = Record { val name: String; val age: Int }

val person = Record("name" -> "Emma", "age" -> 42).asInstanceOf[Person]
val n: String = person.name      // fields("name").asInstanceOf[String]
val a: Int = person.age + 1
// person.email                  // compile error: not a member of Person

There is no reflection here at all: person.name is a map lookup plus a cast. The type gives you autocompletion and static checking of field names and types in the code that uses the record. The weak point is the asInstanceOf[Person] that creates it. Nothing verifies that the map really has an Int under "age", so a wrong row fails later, at the first use, with a ClassCastException or NoSuchElementException.

Libraries that build records over database rows or JSON close that gap by generating the structural type and a validating constructor at compile time with macros, the technique described in Scala 3 metaprogramming. If you hand-write records, put the cast in exactly one factory that checks every declared field's presence and runtime class, and make the factory return an Either so a bad row is a value, not a later crash. Note also that Selectable has no updateDynamic: structural records are read-only through the structural type, unlike scala.Dynamic.

The documented limitations

The Scala 3 reference lists restrictions that follow from compiling a structural call to a name plus erased argument classes:

LimitationWhat it means in practice
Dependent methods cannot be called structurallyA refinement method whose result type mentions its own parameters (def get(k: Key): k.Value) has no erased form to dispatch on
Refinements may not introduce overloadsIf the parent already has a method of that name, the refinement must properly override it, not add a sibling overload
Parameter erasures must matchThe refinement's parameter types must erase to the same classes as the implementation's, or the reflective lookup finds nothing
No updateDynamicAssignment through a structural type is not supported for Selectable

Two more traps come from erasure rather than from the spec. A pattern match case r: { def close(): Unit } => cannot be checked at runtime, because the JVM sees only Object; the compiler warns that it is unchecked, and it matches anything. And generic signatures are erased: if an unchecked cast (such as an unvalidated record factory) lets an object through, a refinement declaring def put(xs: List[Int]): Unit will happily dispatch to its put(List[String]) at runtime. Generics in general are covered in the Scala type system.

What a structural call costs

A reflective structural call does several things a virtual call does not: a method lookup by name and parameter classes (cached after the first call, but the cache is still consulted), boxing of primitive arguments into an Array[Any], a reflective invoke that the JIT optimises far less well than a direct call, unboxing and a cast on the result, and exception handling around the reflective invoke. A custom Selectable costs whatever its selectDynamic does, typically a hash lookup and a cast. Do not trust a number from a blog post, including this one; measure your JVM and your call shape:

import org.openjdk.jmh.annotations.*
import scala.reflect.Selectable.reflectiveSelectable

trait Sized { def size: Int }
class Box(val n: Int) extends Sized { def size: Int = n }

@State(Scope.Thread)
class StructuralBench:
  val nominal: Sized = Box(3)
  val structural: { def size: Int } = Box(3)

  @Benchmark def nominalCall(): Int = nominal.size
  @Benchmark def structuralCall(): Int = structural.size

Run it with sbt-jmh at a realistic call-site shape (one receiver class, then several) before deciding. The usual result is that the difference is irrelevant for a call made once per request and decisive for a call made inside a per-element loop.

Worked example: from duck-typed close to a type class

A service has a helper withResource[R <: { def close(): Unit }] used for a JDBC connection, a vendor client with close() but no interface, and an in-house pool. A profiler shows the helper on a per-row path through a batch importer. The fix keeps the call sites' shape and moves dispatch to compile time with a type class, which is exactly how scala.util.Using already works through its Releasable type class:

import scala.util.Using
import scala.util.Using.Releasable

// AutoCloseable already has a Releasable instance; add the vendor type once.
given Releasable[VendorClient] with
  def release(c: VendorClient): Unit = c.close()

given Releasable[LegacyPool] with
  def release(p: LegacyPool): Unit = p.shutdown()   // a different name is fine

def importRows(client: VendorClient): Int =
  Using.resource(client) { c =>
    c.rows().foldLeft(0)((n, row) => n + write(row))
  }

Three things improved. Dispatch is a resolved given instead of reflection. The legacy pool, whose release method is called shutdown, fits without an adapter class, which the structural type could never express. And a type with no instance is a compile error that names the missing Releasable, not a runtime reflection failure. How the compiler finds those instances is explained in Scala implicit resolution.

Choosing a tool

NeedUseWhy
Several of your own classes share behaviourA traitNominal, fastest, documents intent
Behaviour for classes you cannot changeA type classStatic dispatch, can rename and adapt
One-off adaptation, cold path, Scala 3reflectiveSelectableNo boilerplate; the import flags the cost
Typed view over maps, rows, JSONCustom Selectable recordStatic field checks, no reflection
Narrow a type memberType refinementFree at runtime
Arbitrary member names at runtimescala.DynamicUntyped by design; supports updateDynamic

The failure modes cluster on the same few mistakes: a reflective call inherited from an inferred anonymous-class type in a hot loop, a record cast that is never validated, a pattern match on a refinement that silently matches everything, and a refinement on generic parameters that erasure makes meaningless. Each is cheap to find with a search for reflectiveCalls, reflectiveSelectable and asInstanceOf near Selectable.

What to do next

  1. Search your build for reflectiveCalls and reflectiveSelectable; list every structural call site and whether it is on a per-request or per-element path.
  2. Annotate vals initialised with anonymous classes with a named trait so no structural type is inferred.
  3. Replace hot-path structural calls with a trait you own or a type class such as Releasable.
  4. For records, put the asInstanceOf in one validating factory returning Either.
  5. Remove any pattern match on a refinement type; match on a nominal type instead.
  6. If you keep a structural call, add a test for each concrete runtime class it must accept.
  7. Benchmark the remaining call shapes with the JMH harness above before arguing about cost.
Key takeaway: A structural type is a refinement that adds a term member the parent type lacks. The compiler still type-checks every call, but the dispatch is dynamic: Java reflection in Scala 2 behind reflectiveCalls, and a rewrite to selectDynamic or applyDynamic on a Selectable in Scala 3. Use reflectiveSelectable for rare cold-path adaptation, custom Selectable records for typed views over dynamic data with one validating factory, and a trait or type class everywhere performance or clarity matters.