Skip to content
AgileLogics
7 min read

Type Classes

Prerequisites: Givens and using clauses, Extension methods

A type class defines behavior that types can opt into, without inheritance. It's how Scala adds capabilities to types you don't control — like making Int, String, and your own case classes all "JSON-encodable".

// 1. The type class: a trait with a type parameter
trait JsonEncoder[A]:
  def encode(value: A): String

// 2. Instances for specific types
object JsonEncoder:
  given JsonEncoder[Int] with
    def encode(value: Int): String = value.toString

  given JsonEncoder[String] with
    def encode(value: String): String = "\"" + value.replace("\"", "\\\"") + "\""

  given JsonEncoder[Boolean] = b => b.toString

  // An instance built from another instance
  given [A](using enc: JsonEncoder[A]): JsonEncoder[List[A]] =
    list => list.map(enc.encode).mkString("[", ",", "]")

// 3. A nice syntax via an extension method
extension [A](value: A)(using enc: JsonEncoder[A])
  def toJson: String = enc.encode(value)

@main def typeClasses(): Unit =
  println(42.toJson)
  println("say \"hi\"".toJson)
  println(List(true, false).toJson)
  println(List(List(1, 2), List(3)).toJson)
42
"say \"hi\""
[true,false]
[[1,2],[3]]
  • The instances live in the companion object, so they're found automatically — no import needed.
  • A single-method trait can be implemented with a lambda (b => b.toString).
  • The List instance works for any A that has an encoder — the compiler assembles JsonEncoder[List[List[Int]]] from smaller pieces.
  • Asking for a type with no instance is a compile-time error, not a runtime surprise.

Your own types

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

case class Money(amount: BigDecimal, currency: String)

object Money:
  given Show[Money] with
    extension (m: Money) def show: String = s"${m.currency} ${m.amount.setScale(2)}"

def printAll[A: Show](items: List[A]): Unit =
  items.foreach(item => println(item.show))

@main def showing(): Unit =
  printAll(List(Money(BigDecimal(5), "EUR"), Money(BigDecimal("19.9"), "USD")))
EUR 5.00
USD 19.90

[A: Show] is a context bound — shorthand for "there must be a given Show[A]".

Deriving instances

Writing instances for every case class is tedious. Scala 3 can derive them:

// illustrative
case class User(name: String, age: Int) derives upickle.default.ReadWriter
// or with circe:
case class Order(id: Int, total: BigDecimal) derives io.circe.Codec

Libraries implement derives with Scala 3's Mirror type, which describes a case class's fields at compile time. Type classes power JSON libraries, Ordering, Numeric, and the whole of Cats and ZIO.