Extension Methods

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 a feature depends on a release, the page names it. The import rule of SIP-54 arrived in 3.4, and the move of the primitive Rich* wrappers to extension methods in 3.10; the statements about 3.9 come from compiling the same code with 3.9.0.

An extension method adds a method to a type after the type has been defined, without subclassing it and without wrapping it. The call circle.circumference looks like a member call, and the compiler turns it into the call of an ordinary method that takes the receiver as its first argument. In Scala 3 this is a feature of the language with its own keyword, extension, and it replaces the Scala 2 idiom of an implicit class. Extension methods are also the way type classes expose their operations (see Type Classes), and they depend on the instances described in Givens & Using Clauses and Context Bounds & Deferred Givens. The examples use the trailhead scenario, a toolkit that plans hikes on a network of mountain trails. Readers coming from Kotlin should read the comparison at the end together with Kotlin Extension Functions & Scope Functions.

Defining and calling an extension method

An extension has a leading parameter, the receiver, in parentheses after the extension keyword, followed by one or more def members. The members are the methods that are added.

//> using scala 3.10.0

package trailhead

case class Trail(name: String, lengthKm: Double, ascentM: Int)

extension (t: Trail)
  def steepness: Double = t.ascentM / (t.lengthKm * 1000)
  def label: String = s"${t.name} (${t.lengthKm} km)"

@main def run(): Unit =
  val ridge = Trail("Ridge", 8.5, 900)
  println(ridge.label)
  println(math.round(ridge.steepness * 1000))
  // the extension is an ordinary method taking the receiver first
  println(steepness(ridge) == ridge.steepness)
  println(label(ridge))
Ridge (8.5 km)
106
true
Ridge (8.5 km)

The definition of steepness translates to a method that takes the receiver in a first parameter list, here steepness(t: Trail): Double, labelled as an extension by the compiler. The label matters for the call syntax and for resolution, not for the calling convention: the method can also be called as steepness(ridge). extension is a soft keyword. It is recognized only at the start of a statement and when it is followed by [ or (, so existing code with a method or value named extension keeps compiling.

An extension can be written wherever a definition can: at the top level, in an object, in a trait, in a class, in a given or in a block. The next sections show where that choice matters.

Operators, unary operators and infix

The extension syntax defines operators like any method, with a symbolic name, and infix marks an alphanumeric method that may be written in infix form. A prefix operator is the method unary_- (and unary_+, unary_!, unary_~).

//> using scala 3.10.0

package trailhead

case class Km(value: Double)

extension (a: Km)
  def +(b: Km): Km = Km(a.value + b.value)
  def unary_- : Km = Km(-a.value)
  infix def max(b: Km): Km = if a.value >= b.value then a else b

extension (x: Int)
  def km: Km = Km(x)

@main def run(): Unit =
  val a = Km(2) + Km(3.5)
  println(a)
  println(-a)
  println(Km(1) max Km(4))
  println(3.km + 4.km)
Km(5.5)
Km(-5.5)
Km(4.0)
Km(7.0)

Because an extension can target any type, including types the library does not own, 3.km works on the built-in Int. A method with the name of an existing member of the receiver is another matter (see Members win over extensions).

Right-associative extension methods

An operator whose name ends in a colon is right-associative: x : xs` means `xs.:(x), so the right operand is the receiver. For an extension method the same rule holds, which means the roles of the two parameters are swapped relative to how they are written. The method below is declared with the left operand in the extension header and the right operand as its own parameter, and used infix; the compiler applies the swap.

//> using scala 3.10.0

package trailhead

case class Pack(items: List[String])

extension (x: String)
  def +:(p: Pack): Pack = Pack(x :: p.items)
  def ::>(n: Int): String = x * n

@main def run(): Unit =
  println("a" +: Pack(List("b", "c")))
  println(+:(Pack(List("b", "c")))("a"))   // called as a function: the operands swap back
  println("go" ::> 3)
Pack(List(a, b, c))
Pack(List(a, b, c))
gogogo

"a" : pack` is the same as `pack.:("a") and resolves to the extension with pack as its right-hand parameter. When the same method is called in non-infix form the order is the expanded one: +:(pack)("a"). The expansion of the full signature is leftTyParams leadingUsing rightTyParams rightParam leftParam trailingUsing rest, which matters when parameters depend on each other. A name ending in : followed by a single explicit parameter is what makes the method right-associative; ::> ends in > and is an ordinary left-associative operator.

Generic extensions

An extension can have type parameters, written between extension and the receiver. The type parameters can carry bounds, including context bounds, and the extension can take using clauses before or after the receiver.

//> using scala 3.10.0

package trailhead

extension [T](xs: List[T])
  def second: T = xs.tail.head
  def sumBy[U: Numeric](f: T => U): U = xs.map(f).sum

extension [T: Numeric](x: T)
  def +!(y: T): T = summon[Numeric[T]].plus(x, y)

extension [T](x: T)(using n: Numeric[T])
  def twice: T = n.plus(x, x)

@main def run(): Unit =
  println(List(1, 2, 3).second)
  println(List("a", "bb", "ccc").sumBy[Int](_.length))      // type argument of the method
  println(sumBy[String](List("a", "bb", "ccc"))(_.length))  // type argument of the extension
  println(sumBy[String](List("a", "bb"))[Int](_.length))    // both
  println(2 +! 5)
  println(1.5.twice)
2
6
6
3
7
3.0

Type arguments that match the method’s own type parameters are written as usual after the method name (sumBy[Int]). Type arguments for the parameters that follow extension can only be passed when the extension is called as an ordinary method (sumBy[String](xs)(f)). Passing both on the dotted form is an error, because the dotted form has only the method’s own parameter list to put them in:

Too many type arguments for [U](f: T => U)(using evidence$1: Numeric[U]): U
expected: [U]
actual:   [String, Int]

The second extension above, extension [T: Numeric](x: T), uses a context bound on the extension’s type parameter. It is equivalent to the using clause of the third extension, with the witness named n.

Collective extensions

When several methods share the same receiver, the receiver (and its type parameters and using clauses) can be written once, with all the methods indented under it, or enclosed in braces. This is a collective extension. Methods in the same collective extension can call each other without naming the receiver: the call is rewritten with the shared receiver.

//> using scala 3.10.0

package trailhead

extension (ss: Seq[String])
  def longestStrings: Seq[String] =
    val maxLength = ss.map(_.length).max
    ss.filter(_.length == maxLength)
  def longestString: String = longestStrings.head      // receiver `ss` is implicit

extension [T](xs: List[T])(using Ordering[T])
  def smallest(n: Int): List[T] = xs.sorted.take(n)
  def smallestIndices(n: Int): List[Int] =
    val limit = smallest(n).max
    xs.zipWithIndex.collect { case (x, i) if summon[Ordering[T]].lteq(x, limit) => i }

extension (s: String)
  def position(ch: Char, n: Int): Int =
    if n < s.length && s(n) != ch then position(ch, n + 1) else n

@main def run(): Unit =
  println(Seq("a", "bcd", "ef", "ghi").longestStrings)
  println(Seq("a", "bcd", "ef", "ghi").longestString)
  println(List(5, 1, 4, 1).smallest(2))
  println(List(5, 1, 4, 1).smallestIndices(2))
  println("hello".position('l', 0))
List(bcd, ghi)
bcd
List(1, 1)
List(1, 3)
2

The collective form is a shorthand for one extension per method: longestString expands to a separate extension that calls ss.longestStrings.head. The rewrite also applies to a recursive call, so position(ch, n + 1) in the last extension is s.position(ch, n + 1). The braces form (extension (ss: Seq[String]) { ... }) is accepted for Scala 2 style code and may contain indented regions.

How a call is resolved

For a selection r.m where m is not a member of r, the compiler looks for an extension method named m that is applicable at that point. There are four ways for an extension to be applicable:

  1. The extension is visible under a simple name: defined, inherited or imported in a scope enclosing the reference.

  2. The extension is a member of a given instance that is visible at the reference.

  3. The reference is of the form r.m and the extension is defined in the implicit scope of the type of r (for example the companion object of `r’s class).

  4. The reference is of the form r.m and the extension is defined in a given instance that is in the implicit scope of the type of r.

//> using scala 3.10.0

package trailhead

trait IntOps:
  extension (i: Int) def isZero: Boolean = i == 0
  extension (i: Int) def safeMod(x: Int): Option[Int] =
    if x.isZero then None else Some(i % x)       // rule 1: same scope

object IntOpsEx extends IntOps:
  extension (i: Int) def safeDiv(x: Int): Option[Int] =
    if x.isZero then None else Some(i / x)       // rule 1: inherited

trait SafeDiv:
  import IntOpsEx.*                              // rule 1: imported
  extension (i: Int) def divide(d: Int): Option[(Int, Int)] =
    (i.safeDiv(d), i.safeMod(d)) match
      case (Some(q), Some(r)) => Some((q, r))
      case _ => None

object Div extends SafeDiv

case class Route(legs: List[String])
object Route:
  extension (r: Route) def length: Int = r.legs.size                // rule 3
  extension (rs: List[Route]) def allLegs: List[String] = rs.flatMap(_.legs)
  given Ordering[Route] with                                         // rule 4
    def compare(a: Route, b: Route) = a.legs.size - b.legs.size
    extension (a: Route) def longerThan(b: Route): Boolean = compare(a, b) > 0

@main def run(): Unit =
  import Div.*
  println(7.divide(2))
  println(7.divide(0))
  given ops1: IntOps()                           // rule 2: member of a given
  println(7.safeMod(4))
  val r = Route(List("a", "b", "c"))
  println(r.length)                              // found without an import
  println(List(r, Route(List("z"))).allLegs)
  println(r.longerThan(Route(List("x"))))
Some((3,1))
None
Some(3)
3
List(a, b, c, z)
true

The extensions in the companion of Route are found with no import, because the companion belongs to the implicit scope of Route. The one defined for List[Route] is found for the same reason, since Route is part of the type List[Route]. This is the reason a library puts its operations in the companion: users get them for free. The longerThan method lives in a given that is itself in the implicit scope of Route.

The precise rule for a selection e.m[Ts] where m is not a member of e is two rewritings, tried in order. First, the selection is rewritten to m[Ts](e) and typechecked with the ordinary name resolution (with a relaxation for imports, see below). If that does not typecheck, and there is an extension named m in an eligible object o (an object in the implicit scope of the type of e, or a visible given, or a given in that implicit scope), the selection is rewritten to o.m[Ts](e). The second step happens at the same time the compiler would try an implicit conversion, and more than one way of rewriting is an ambiguity error.

flowchart TD A["r.m where m is not a member of r"] --> B{"First rewriting m(r)<br/>typechecks?"} B -- yes --> C["use m(r)"] B -- no --> D{"Eligible object o has<br/>an extension m?"} D -- yes --> E["use o.m(r)"] D -- no --> F["error: m is not a member of r"]

The diagram shows the order only. A name that is found at the first step but fails to typecheck falls through to the second step. If no step succeeds the compiler reports "value m is not a member" (for instance value total is not a member of List[Int] when no total is in scope).

Members win over extensions

An extension is considered only when the receiver has no member of that name. A member that is applicable to the arguments always wins, even if an extension with the same name is in scope. The compiler tells you when you wrote an extension that can never be chosen.

//> using scala 3.10.0

package trailhead

class Counter(val n: Int):
  def describe: String = "member"

extension (c: Counter)
  def describe: String = "extension"
  def twice: Int = c.n * 2

@main def run(): Unit =
  val c = Counter(2)
  println(c.describe)       // the member wins
  println(describe(c))      // the extension, called as a function
  println(c.twice)
Extension method describe will never be selected from type Counter
because Counter already has a member with the same name and compatible parameter types.
member
extension
4

The first two lines are the compiler warning; the last three are the output of the program. The consequence is that an extension cannot override or change a class: it can only add what is missing, and it is only an addition within the scope in which it is applicable.

Importing the same name from several sources (SIP-54, 3.4)

Before Scala 3.4, importing two extensions with the same name from two objects was an "ambiguous import" error, even when the receiver types differed. SIP-54 relaxes the rule for extension calls: if the name is imported from several places on the same nesting level, the compiler tries each import as an extension method, and if only one expansion typechecks it picks that expansion.

//> using scala 3.10.0

package trailhead

class Foo
class Bar

object A:
  extension (foo: Foo) def meth(): String = "Foo"
  def normalMeth(foo: Foo): String = "A.normalMeth"
object B:
  extension (bar: Bar) def meth(): String = "Bar"
  def normalMeth(bar: Bar): String = "B.normalMeth"

@main def run(): Unit =
  import A.*
  import B.*
  println(Foo().meth())
  println(Bar().meth())
Foo
Bar

Each call resolves to the extension for its own receiver. The relaxation applies only when the method is used as an extension. Written as a normal method in prefix form, the same name stays ambiguous, as for any two imports:

Reference to meth is ambiguous.
It is both imported by import trailhead.A._
and imported subsequently by import trailhead.B._

 Hint: This error may arise if extension method `meth` is called as a normal method.

If two imports both lead to an expansion that typechecks, the call is ambiguous, unless exactly one of the imports is not a wildcard import, in which case that one wins:

//> using scala 3.10.0

package trailhead

object A:
  extension (s: String) def shout: String = s.toUpperCase + "!"
object B:
  extension (s: String) def shout: String = s.toUpperCase + "!!"

@main def run(): Unit =
  import A.shout
  import B.*
  println("hi".shout)
HI!

With two wildcard imports, import A.* and import B.*, the same call stops compiling:

value shout is not a member of String.
An extension method was tried, but could not be fully constructed:

    trailhead.B.shout("hi")

    failed with:

        Ambiguous extension methods:
        both trailhead.B.shout("hi")
        and  trailhead.A.shout("hi")
        are possible expansions of "hi".shout

The motivating case of the SIP is a / operator that is defined for both java.nio.file.Path and java.net.URI: the two extensions used to live in a flat namespace and could not be imported together.

Erasure clashes and @targetName

Extensions on different instantiations of the same generic type erase to the same JVM signature, so they cannot both be defined with the same name:

//> using scala 3.10.0

package trailhead

extension (xs: List[Int]) def total: Int = xs.sum
extension (xs: List[String]) def total: Int = xs.map(_.length).sum
Conflicting definitions:
def total(xs: List[Int]): Int in the top-level definitions in package trailhead at line 3 and
def total(xs: List[String]): Int in the top-level definitions in package trailhead at line 4
have the same type (xs: List): Int after erasure.

Consider adding a @targetName annotation to one of the conflicting definitions
for disambiguation.

The @targetName annotation gives the second method another name in the bytecode while the Scala name is kept, so both extensions still resolve by receiver type:

//> using scala 3.10.0

package trailhead

import scala.annotation.targetName

extension (xs: List[Int]) def total: Int = xs.sum
extension (xs: List[String])
  @targetName("totalLength") def total: Int = xs.map(_.length).sum

@main def run(): Unit =
  println(List(1, 2, 3).total)
  println(List("a", "bb").total)
6
3

Extensions on your own types: opaque types and companions

Extension methods are the way an opaque type exposes behaviour. Inside the scope that defines the opaque type the alias is transparent, so the extensions can use the underlying Double or Int; outside, only the extensions are visible. See Opaque Types & Value Classes for the type itself.

//> using scala 3.10.0

package trailhead

object Units:
  opaque type Km = Double
  opaque type Minutes = Int

  object Km:
    def apply(d: Double): Km = d
  object Minutes:
    def apply(m: Int): Minutes = m

  extension (a: Km)
    def +(b: Km): Km = a + b
    def value: Double = a
    def toMiles: Double = a * 0.621371
  extension (m: Minutes)
    def +(n: Minutes): Minutes = m + n
    def hours: Double = m / 60.0
  extension (d: Double) def km: Km = Km(d)
  extension (n: Int) def min: Minutes = Minutes(n)

import Units.*

@main def run(): Unit =
  val d = 4.5.km + 2.0.km
  println(d.value)
  println(f"${d.toMiles}%.2f".replace(',', '.'))
  println((90.min + 30.min).hours)
6.5
4.04
2.0

The + inside Units refers to the Double or Int addition because the aliases are transparent there. The km and min extensions on the primitive types are visible only after import Units.*.

What the bytecode looks like

An extension is a normal method whose first parameter is the receiver, so Java can call it directly. The example in the first section, compiled from a file H.scala with an extension on a Circle case class, produced the following class shape (javap -p):

public final class trailhead.H$package {
  public static double circumference(trailhead.Circle);
  public static void run();
}

The top-level extension lives in the generated <file>$package class. Java code calls it as H$package.circumference(circle); the $package name comes from the source file name. Kotlin extension functions compile the same way, to a static method in a generated file class whose name ends in Kt.

Extension methods versus implicit classes

In Scala 2 (and still on Scala 3 with implicit class) the same job is done with an implicit class: a wrapper class with a constructor taking the receiver, plus an implicit conversion that the compiler inserts when a member is missing.

//> using scala 3.10.0

package trailhead

// Scala 2 style: an implicit class
implicit class RichTrail(val t: String) extends AnyVal:
  def shout: String = t.toUpperCase + "!"

// Scala 3 style
extension (t: String) def whisper: String = t.toLowerCase + "..."

@main def run(): Unit =
  println("ridge".shout)
  println("RIDGE".whisper)
RIDGE!
ridge...

The two compile to the same effect at run time, and extends AnyVal was needed to avoid allocating the wrapper. The differences are in the language: the extension has no class, no constructor and no conversion to resolve, its signature reads as what it is, it supports type parameters and using clauses on the receiver, it can be collective, and it has a clearer resolution rule (the four applicability rules above, not "any implicit view that supplies the member"). Because it is not a conversion, an extension cannot be used where the wrapper type is expected. Implicit classes are still accepted by Scala 3.10, and Scala 2 cross-built code keeps using them.

Scala 3.10: the Rich wrappers become extension methods

Until 3.9 the operations of Int such as to, until, max, min and abs came from the implicit wrapper class scala.runtime.RichInt, inserted by the Predef.intWrapper conversion. Scala 3.10 removes the Rich* implicit classes for the primitive types: the operations are extension methods defined in the companion objects of Int, Long and the other primitives. Call sites such as 1 to 10 keep compiling unchanged. The typed tree of the same two lines shows the change (printed with -Vprint:typer):

3.9.0:   val r: Range.Inclusive = intWrapper(1).to(4)
         val m: Int = intWrapper(3).max(9)
3.10.0:  val r: Range.Inclusive = Int.to(1)(4)
         val m: Int = Int.max(3)(9)

The news page for the release lists the effects: macros and compiler plugins that looked at the old Rich* symbols need to match the extension shape, and the a -> b pair syntax is now an inline extension method too, so a DSL that defines its own -> with an implicit class should change it to an extension method, because the two are prioritized differently and Predef.-> may now be chosen over the implicit class. A -> defined as an extension method still takes precedence. This is the standard library applying to itself the advice of this page.

Kotlin and Java comparison

Kotlin extension functions (fun String.shout()) have the same goal and the same static compilation. Their resolution differs: a Kotlin extension is found by name in the imported scope (the file’s own package, explicit imports and default imports), and a member always wins, as in Scala. Scala adds the implicit-scope rules, so an extension placed in a type’s companion or in a given in the implicit scope is available everywhere without an import. Scala extensions can be generic with context bounds and using clauses on the receiver, and can be grouped as collective extensions; Kotlin groups by file or by a class or object that acts as a dispatch receiver. Kotlin extension properties exist (val String.lastChar: Char); Scala has no extension properties, but a parameterless extension method def lastChar is called without parentheses and reads like one.

Java has no extension methods. The usual substitutes are static utility methods (Collections.max(list)) or default methods on an interface that the type implements. Neither can add a method to a type you do not own, which is the Scala extension’s purpose.

Scala 3.9 LTS versus 3.10

  • The extension syntax, collective extensions, resolution rules, @targetName and right-associative extensions are the same on 3.9 and 3.10; they arrived with Scala 3.0.

  • SIP-54 (multi-source extension overloads) arrived in 3.4, so it is also available on the 3.9 LTS; it is not part of the 3.3 LTS line (from the SIP, not compiled here).

  • The Rich* change is 3.10 only: on 3.9.0 the typed tree still shows intWrapper(1).to(4).

  • The implicit class form still compiles on 3.10 (checked above).

Where the books are outdated

The Scala 3 Book chapter on extension methods predates SIP-54 and describes the rules at the level of its examples; the Reference is the authority for the resolution steps. The Overviews page on implicit classes describes the Scala 2.10 feature and does not mention that the standard library’s own Rich* wrappers are no longer implicit classes on 3.10.