Scala 3 compatibility layer for scala-newtype.
scala-newtype provides @newtype and @newsubtype macro annotations for zero-cost wrapper types in Scala 2. This project makes them work on Scala 3 by providing a compiler plugin that performs the same rewrite the Scala 2 macro did.
Work in progress. Tested on Scala 2.13.16 and all Scala 3 versions from 3.3.0 to 3.8.2.
// build.sbt
// Brings io.estatico:newtype onto the classpath (both Scala 2.13 and 3)
libraryDependencies += "com.kubuszok" %% "newtype-compat" % "<version>"
// Scala 3 only: compiler plugin that rewrites @newtype annotations
libraryDependencies ++= {
if (scalaBinaryVersion.value == "3")
Seq(compilerPlugin("com.kubuszok" %% "newtype-plugin" % "<version>"))
else Nil
}
// Scala 2.13: enable macro annotations
scalacOptions ++= {
if (scalaBinaryVersion.value == "2.13") Seq("-Ymacro-annotations")
else Nil
}Your existing @newtype code works unchanged on both Scala 2.13 and 3:
import io.estatico.newtype.macros.newtype
import io.estatico.newtype.ops._
@newtype case class UserId(value: Int)
val id: UserId = UserId(42)
val raw: Int = id.coerce[Int] // 42
id.value // 42@newtype case class Nel[A](toList: List[A])
val xs: Nel[Int] = Nel(List(1, 2, 3))
xs.coerce[List[Int]] // List(1, 2, 3)@newtype case class Score(value: Int) {
def add(n: Int): Score = Score(value + n)
}
Score(10).add(5) // Score(15)@newsubtype case class PosInt(value: Int)
// PosInt is a subtype of Int at the type levelimport cats._
@newtype case class Name(value: String)
object Name {
implicit val eq: Eq[Name] = deriving
implicit val show: Show[Name] = deriving
}@newtype(unapply = true) case class Token(value: String)
Token("abc") match {
case Token(s) => s // "abc"
}All newtypes generate Coercible instances for zero-cost conversions:
import io.estatico.newtype.Coercible
@newtype case class Email(value: String)
val wrap = implicitly[Coercible[String, Email]]
val unwrap = implicitly[Coercible[Email, String]]
wrap("[email protected]") // Email("[email protected]")- On Scala 2.13, the original
@newtypemacro annotation does the transformation at compile time. - On Scala 3, the
newtype-plugincompiler plugin runs after the parser but before the typer. It detects@newtype/@newsubtypeannotated case classes and rewrites them into the same expanded form the Scala 2 macro would produce: a type alias + companion object withCoercibleimplicits, accessor methods, and deriving support.
Both Scala 2.13 and 3 depend on the same io.estatico:newtype_2.13 artifact for the runtime types (Coercible, CoercibleIdOps, etc.). Scala 3 can consume Scala 2.13 jars natively.
| Module | Description | Scala versions |
|---|---|---|
newtype-compat |
Empty artifact that brings in io.estatico:newtype_2.13:0.4.4 |
2.13, 3.3.x - 3.8.x |
newtype-plugin |
Scala 3 compiler plugin | 3.3.x - 3.8.x |
- The generated
Opsimplicit class does not extendAnyValon Scala 3 (value classes with abstract type members cause codegen issues in dotty). @newtypeinside local scopes (e.g. inside a method body) is not supported.
Apache 2.0, same as the original scala-newtype.