Newtypes
Type-safe wrappers.
Newtypes is a free Scala for Backend Engineering & Functional Programming lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Scala for Backend Engineering & Functional Programming learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Type-Safe Wrappers
A newtype is a distinct type that wraps a single underlying value. It prevents mixing up values that share a primitive representation, like a UserId and a ProductId that are both Ints.
case class UserId(value: Int)
case class ProductId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val u = UserId(1)
val p = ProductId(1)
println(u.value == p.value) // values equal, types differThe Primitive Obsession Problem
Primitive obsession is overusing String and Int for domain concepts. It allows nonsense like passing an email where a name belongs. Newtypes fix this by giving each concept its own type.
case class Email(value: String)
case class City(value: String)
object Main:
def greet(c: City): String = s"Welcome to ${c.value}"
def main(args: Array[String]): Unit =
println(greet(City("Berlin")))Case Class Newtypes
The simplest newtype is a single-field case class. It gives equality, a readable toString, and pattern matching out of the box. The cost is one object allocation per value.
case class OrderId(value: String)
object Main:
def main(args: Array[String]): Unit =
val id = OrderId("ORD-42")
println(id)
println(id.value)Opaque Type Newtypes
For zero allocation, implement a newtype with an opaque type. It behaves like the underlying type at runtime but is distinct at compile time.
object Ids:
opaque type UserId = Int
def apply(i: Int): UserId = i
extension (u: UserId) def value: Int = u
object Main:
def main(args: Array[String]): Unit =
val u = Ids(7)
println(u.value)Adding Behavior with Extensions
Give a newtype operations through extension methods, exposing only what makes sense for the domain concept.
object Money:
opaque type Cents = Long
def apply(n: Long): Cents = n
extension (c: Cents)
def +(o: Cents): Cents = c + o
def toDollars: Double = c / 100.0
object Main:
def main(args: Array[String]): Unit =
val total = Money(250) + Money(750)
println(total.toDollars)Newtypes in Function Signatures
Newtypes make signatures self-documenting and catch argument-order bugs at compile time. You cannot accidentally swap two parameters of different newtypes.
case class Width(value: Int)
case class Height(value: Int)
object Main:
def area(w: Width, h: Height): Int = w.value * h.value
def main(args: Array[String]): Unit =
println(area(Width(4), Height(5)))Newtypes and Collections
A Map keyed by a newtype is clearer and safer than one keyed by a raw Int. The type prevents using the wrong kind of key.
case class UserId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val names = Map(UserId(1) -> "Ada", UserId(2) -> "Bob")
println(names(UserId(2)))Validated Newtypes
Combine a newtype with a smart constructor so the wrapper also enforces invariants. Here a NonEmptyString can never be empty.
case class NonEmptyString private (value: String)
object NonEmptyString:
def of(s: String): Option[NonEmptyString] =
if s.nonEmpty then Some(NonEmptyString(s)) else None
object Main:
def main(args: Array[String]): Unit =
println(NonEmptyString.of("hi"))
println(NonEmptyString.of(""))Choosing a Representation
Pick based on your needs.
- case class: easiest, pattern-matchable, allocates an object.
- opaque type: zero-cost, no boxing, ideal for hot paths and big collections.
object Temp:
opaque type Kelvin = Double
def apply(d: Double): Kelvin = d
extension (k: Kelvin) def value: Double = k
object Main:
def main(args: Array[String]): Unit =
val readings = List(Temp(300.0), Temp(310.5))
println(readings.map(_.value).sum)Preventing Accidental Conversions
Two newtypes over the same base do not implicitly convert. To go between them you write an explicit function, documenting the intent.
case class Meters(value: Double)
case class Feet(value: Double)
object Main:
def toFeet(m: Meters): Feet = Feet(m.value * 3.281)
def main(args: Array[String]): Unit =
println(toFeet(Meters(2.0)))When to Use Newtypes
Reach for newtypes whenever a primitive carries domain meaning.
- Identifiers, units, formatted strings.
- Use case class for simplicity, opaque type for performance.
- Add a smart constructor when there are invariants.
object Domain:
opaque type Sku = String
def of(s: String): Option[Sku] =
if s.startsWith("SKU-") then Some(s) else None
extension (k: Sku) def raw: String = k
object Main:
def main(args: Array[String]): Unit =
println(Domain.of("SKU-9").map(_.raw))Quick Check
Test your understanding of newtypes.
Recap
You learned newtypes.
- Newtypes wrap one underlying value in a distinct type.
- They cure primitive obsession and prevent argument mixups.
- Use
case classfor simplicity,opaque typefor zero cost. - Add a smart constructor for validated wrappers.
- Conversions between newtypes are always explicit.
object Ids:
opaque type AccountId = Long
def apply(n: Long): AccountId = n
extension (a: AccountId) def value: Long = a
object Main:
def main(args: Array[String]): Unit =
val acc = Ids(1001L)
println(acc.value)Frequently asked questions
Is the “Newtypes” lesson free?
Yes — the full text of “Newtypes” is free to read here on the web, and the Scala for Backend Engineering & Functional Programming course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Scala for Backend Engineering & Functional Programming course, upgrade to CoddyKit PRO.
What will I learn in “Newtypes”?
Type-safe wrappers. You practise Scala for Backend Engineering & Functional Programming with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Scala for Backend Engineering & Functional Programming?
No prior experience is required. Scala for Backend Engineering & Functional Programming on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Newtypes” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Scala for Backend Engineering & Functional Programming lesson?
Yes. Every Scala for Backend Engineering & Functional Programming lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.