Structural Types, Selectable & Dynamic

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 compiled and run with Scala 3.10.0 through Scala CLI on JDK 26 (checked on 2026-10-15). Where an example was also compiled with the 3.9 LTS line (3.9.0) and the 3.3 LTS line (3.3.8), the text says what happened. Most Scala code is typed nominally: a method that needs something with a name takes a trait or class that declares it. A structural type describes only the members a value must have, whatever its class, so { def name: String } accepts a Hut, a Trail and an anonymous object alike. Scala 3 also lets a library decide how such members are looked up, through Selectable, and keeps Dynamic for member names that are not known to the compiler at all. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails. For the nominal alternative see Traits & Mixins and Generics & Bounds; for the other ways of combining types see Union & Intersection Types; for named tuples, which the last section combines with Selectable, see Tuples & Named Tuples.

Refinements and structural types

A refinement adds members to a type: Named { def name: String } is "a Named that also has name`". Written on its own, as in `{ def name: String }, it refines AnyRef and is a structural type because only the shape is stated. A type alias gives it a name. A structural type can contain def, val and type members.

Calling a member that the static type describes but the runtime class does not statically declare needs a way to find it. Scala 3 does not guess; the receiver has to be Selectable or Dynamic. The standard library supplies a ready-made implicit conversion, scala.reflect.Selectable.reflectiveSelectable, which finds the member with Java reflection. The example passes a class, a val member, a method with a parameter and an anonymous object to structural types.

//> using scala 3.10.0
package trailhead

import scala.reflect.Selectable.reflectiveSelectable

class Hut(val name: String, val beds: Int):
  def describe(sleeps: Int): String = s"$name sleeps $sleeps of $beds"

type Named = { def name: String }
type Shelter = { val beds: Int; def describe(sleeps: Int): String }

def label(x: Named): String = x.name.toUpperCase

@main def run(): Unit =
  val h = Hut("Alpine Hut", 20)
  println(label(h))
  val s: Shelter = h
  println(s.beds)
  println(s.describe(12))
  println(label(new { def name = "Anonymous" }))
ALPINE HUT
20
Alpine Hut sleeps 12 of 20
ANONYMOUS

Hut does not extend anything: it only has the right members. A val member in the refinement accepts a val or a def without parameters in the class, and a def accepts a method of the same name and signature. The program printed the same output on 3.3.8 and 3.9.0.

Without the import

Without reflectiveSelectable in scope the member access is an error that names the fix.

//> using scala 3.10.0
package trailhead

class Hut(val name: String)
type Named = { def name: String }
def label(x: Named): String = x.name
@main def run(): Unit = println(label(Hut("A")))
[error] ./s2.scala:6:31
[error] Found:    (x : trailhead.Named)
[error] Required: Selectable | Dynamic
[error]
[error] The following import might fix the problem:
[error]
[error]   import scala.reflect.Selectable.reflectiveSelectable
[error]
[error] def label(x: Named): String = x.name
[error]                               ^

In Scala 2 the same code compiled with a feature warning unless import scala.language.reflectiveCalls was in scope. In Scala 3 the rule is stricter and the import is a conversion rather than a language-feature flag. Importing reflectiveSelectable at the use site makes the cost visible in the code that pays it.

The reflection cost

reflectiveSelectable looks the member up with java.lang.reflect on every call (Java reflection, the reference says so explicitly), which is much slower than a virtual call and fails at run time if the member does not exist or is not accessible. The example calls a structural method 100000 times, and also hands a class with a private name to a method that wants a public one.

//> using scala 3.10.0
package trailhead
import scala.reflect.Selectable.reflectiveSelectable

class Hut(val name: String)
type Named = { def name: String }
def label(x: Named): String = x.name

class Priv private (private val name: String)
@main def run(): Unit =
  println(label(Hut("A")))
  val n = 100000
  val h = Hut("B")
  var i = 0; var len = 0
  while i < n do { len += label(h).length; i += 1 }
  println(len)
  try println(label(Priv.apply())) catch case e: Exception => println(e.getClass.getName)
object Priv:
  def apply() = new Priv("x")
A
100000
java.lang.NoSuchMethodException

The last line is a run-time failure the type checker did not prevent: label(Priv()) compiled, but the reflective lookup found no public name method and threw NoSuchMethodException. Prefer a trait, a type class or a function parameter whenever you control the types; keep structural types for adapters over classes you cannot change, small scripts and tests.

Writing your own Selectable

scala.Selectable is a marker trait with no required members. If a value’s type is a subtype of Selectable, the compiler rewrites a member access that it cannot resolve statically:

  • x.name, for a refinement val name: T or a parameterless def name: T, becomes x.selectDynamic("name").asInstanceOf[T].

  • x.add(2, 3), for a refinement method, becomes x.applyDynamic("add")(2, 3).asInstanceOf[R]. If the class declares applyDynamic(name: String, paramClasses: Class[?]*)(args: Any*), the parameter classes of the refinement method are passed too, so an implementation can pick an overload.

The class chooses the storage: a Map, a JSON tree, a database row, a Java object. A subtype of the class with a refinement gives the compiler something to check against. The first example is a record of fields.

//> using scala 3.10.0
package trailhead

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

type Waypoint = Record { val name: String; val elevation: Int }

@main def run(): Unit =
  val w = Record("name" -> "Col", "elevation" -> 2100).asInstanceOf[Waypoint]
  println(w.name)
  println(w.elevation + 1)
Col
2101

The cast is how a loosely typed value gets the type of its expected shape: the compiler trusts the refinement and the selectDynamic implementation has to deliver it. Accessing a name the refinement does not declare is a compile error, which is the advantage over a Map[String, Any]: with the extra line w.nothing the compiler reports value nothing is not a member of trailhead.Waypoint - did you mean w.notify? or perhaps w.toString?. The refinement is type-checked, the data is not: if the map holds no "elevation" key the failure happens at run time, in selectDynamic. The program compiled and ran on 3.3.8 and on 3.9.0.

Method calls go through applyDynamic. The example dispatches add and greet to functions stored in a table.

//> using scala 3.10.0
package trailhead

class MethodCalls(elems: (String, Seq[Any] => Any)*) extends Selectable:
  private val table = elems.toMap
  def applyDynamic(name: String, ctags: Class[?]*)(args: Any*): Any = table(name)(args)

type Calc = MethodCalls { def add(a: Int, b: Int): Int; def greet(who: String): String }

@main def run(): Unit =
  val c = MethodCalls(
    "add"   -> (args => args(0).asInstanceOf[Int] + args(1).asInstanceOf[Int]),
    "greet" -> (args => s"Hello ${args(0)}")
  ).asInstanceOf[Calc]
  println(c.add(2, 3))
  println(c.greet("trail"))
5
Hello trail

Mutable fields in refinements (Scala 3.5)

A refinement may declare a var, but only since Scala 3.5. Assignments x.units = "mi" are not routed to selectDynamic. They become a call of the setter method units_=, which for a Selectable goes through applyDynamic with the name units_$eq (the JVM encoding of =). The example stores the fields in a mutable map.

//> using scala 3.10.0
package trailhead

import scala.collection.mutable

class MutableRecord(init: (String, Any)*) extends Selectable:
  private val fields = mutable.Map(init*)
  def selectDynamic(name: String): Any = fields(name)
  def applyDynamic(name: String)(args: Any*): Any =
    println(s"applyDynamic($name)")
    if name.endsWith("_$eq") then fields(name.stripSuffix("_$eq")) = args.head
    else fields(name)

type Settings = MutableRecord { var units: String; var zoom: Int }

@main def run(): Unit =
  val s = MutableRecord("units" -> "km", "zoom" -> 10).asInstanceOf[Settings]
  println(s.units)
  s.units = "mi"
  s.zoom = s.zoom + 2
  println(s"${s.units} ${s.zoom}")
km
applyDynamic(units_$eq)
applyDynamic(zoom_$eq)
mi 12

Reads still go to selectDynamic (the first km line has no applyDynamic trace). Writing s.zoom += 2 did not compile on 3.10.0 (value += is not a member of Int), so spell the update out. On 3.3.8 and 3.4.3 the refinement is rejected with refinement cannot be a mutable var. You can use an explicit getter units and setter units_= instead; on 3.5.2 and 3.9.0 the program above printed the same output as on 3.10.0.

Typed fields with named tuples (Scala 3.7)

Instead of casting to a refinement, a Selectable can declare a type member Fields: a named-tuple type that lists its fields and their types. Then w.name and w.elevation are statically typed from Fields with no refinement. This needs named tuples, which are stable in 3.7 and later.

//> using scala 3.10.0
package trailhead

class Row[F <: scala.NamedTuple.AnyNamedTuple](data: Map[String, Any]) extends Selectable:
  type Fields = F
  def selectDynamic(name: String): Any = data(name)

type WaypointRow = Row[(name: String, elevation: Int)]

@main def run(): Unit =
  val w = Row[(name: String, elevation: Int)](Map("name" -> "Col", "elevation" -> 2100))
  val n: String = w.name
  val e: Int = w.elevation
  println(s"$n $e")
  println(w.elevation + 1)
Col 2100
2101

This ran on 3.7.4, 3.9.0 and 3.10.0. On 3.6.4 the named-tuple type is an experimental feature and is rejected, and on 3.3.8 the syntax does not parse. This is the form to use for rows or JSON objects whose fields are known from a schema. Source: Scala 3 Reference: Named Tuples

scala.Dynamic

A class that extends scala.Dynamic can be called with any member name. Unlike Selectable, there is no refinement that declares the names. The compiler rewrites an access it cannot resolve:

Source Rewritten to

x.name

x.selectDynamic("name")

x.name = v

x.updateDynamic("name")(v)

x.name(args)

x.applyDynamic("name")(args)

x.name(label = v)

x.applyDynamicNamed("name")"label", v

The call is rewritten only when no real member of that name exists. A class that extends Dynamic needs import scala.language.dynamics (or the option -language:dynamics), otherwise the compiler reports extension of type scala.Dynamic needs to be enabled. The example answers all four forms.

//> using scala 3.10.0
package trailhead

import scala.language.dynamics
import scala.collection.mutable

class Bag extends Dynamic:
  private val m = mutable.Map.empty[String, Any]
  def selectDynamic(name: String): Any = m.getOrElse(name, s"<no $name>")
  def updateDynamic(name: String)(value: Any): Unit = m(name) = value
  def applyDynamic(name: String)(args: Any*): String = s"$name(${args.mkString(", ")})"
  def applyDynamicNamed(name: String)(args: (String, Any)*): String = s"$name named ${args.mkString(", ")}"

@main def run(): Unit =
  val b = Bag()
  b.title = "Ridge"
  println(b.title)
  println(b.missing)
  println(b.go(1, 2))
  println(b.route(from = "A", to = "B"))
  println(b.title.getClass.getSimpleName)
Ridge
<no missing>
go(1, 2)
route named (from,A), (to,B)
String

All accesses have the static result type that the method declares, so b.title is an Any and b.title.length does not compile (value length is not a member of Any). Dynamic is therefore good for scripting-style DSLs, wrappers over untyped data such as a JSON document, and bridges to dynamic languages, and poor for anything where the compiler should check names. Typos in member names are run-time results rather than compile errors, which is the trade-off to weigh against a typed Selectable.

Dynamic or Selectable

Selectable and Dynamic are related (the Scala 3 Reference has a section on their relation, cited below): Selectable serves names that are declared in a refinement and Dynamic serves names that are not. Choose by who knows the names.

Question Selectable with a refinement Dynamic

Where are the names declared?

In a refinement or Fields type, checked by the compiler

Nowhere; any name compiles

What is the static type of an access?

The type in the refinement

Any (whatever the method returns)

What does a typo cost?

A compile error

A run-time lookup of a name that does not exist

Language import

reflectiveSelectable import for reflection; none for your own class

scala.language.dynamics

Typical use

Records, rows, adapters over classes you cannot change

Scripting DSLs and untyped wrappers

Scala 2, Java, Kotlin and other languages

  • Scala 2. Structural calls worked out of the box with reflection after import scala.language.reflectiveCalls, and a feature warning otherwise. In Scala 3 the call needs the reflectiveSelectable conversion or a Selectable receiver. The Dynamic trait existed in Scala 2.10 and later with the same rewrite rules and the same language import.

  • Java. Types are nominal. Duck typing needs an interface or reflection by hand (getMethod("name").invoke(x)), which is what reflectiveSelectable does for you. See Lambdas & Method References for the usual Java answer, a functional interface.

  • Kotlin. Types are nominal too; the closest tool is an interface plus delegation, or Map delegated properties. Kotlin has no structural type, but see Type-Safe Builders & DSLs for dynamic-looking DSLs.

  • TypeScript. Object types are structural by default, as they are in a structural Scala type, and checked entirely at compile time because the objects are plain JavaScript values. See TypeScript.

  • Python. Duck typing is the default and typing.Protocol describes the shape for a type checker. See Python.

Pitfalls

  • Using reflectiveSelectable in a hot loop. Every call is a reflective lookup (see the 100000-call example).

  • Forgetting that the refinement is trusted. A Record cast to Waypoint throws when the data lacks a field, and a structural type over a class with a private or differently named member fails with NoSuchMethodException.

  • x.f += 1 and similar compound assignment on a Selectable var does not compile; write x.f = x.f + 1.

  • Extending Dynamic without the language import, and treating Dynamic as a way to avoid writing types. Anything that returns Any pushes the casts onto the caller.

  • A structural type in a public API leaks implementation to every client; prefer a trait.

Where the books are outdated

  • Scala 2 material says a structural call is allowed after import scala.language.reflectiveCalls; Scala 3 uses reflectiveSelectable or a Selectable class.

  • Material before 3.5 says a refinement cannot declare a var; it can since 3.5.

  • Material before 3.7 does not show the Fields type member with named tuples.

  • The Scala 3 Book describes Selectable but does not list the 3.5 and 3.7 additions above.

Scala 3.9 LTS versus 3.10

  • The Selectable rewrite, reflectiveSelectable and Dynamic behave the same on 3.3, 3.9 and 3.10 for the programs shown, except that the 3.3 LTS line rejects var refinements (needs 3.5) and named-tuple Fields (needs 3.7).

  • Both 3.9 and 3.10 compile the var refinement and the Fields examples with identical output.