It’s rather boilerplate heavy, as (at least as I understand it) macros can’t currently produce definitions and you have to re-wrap the type after each call, but you can use them in a way that provides access to the methods of the underlying type:
object types {
object Name {
opaque type Type <: String = String
def apply(s: String): Type = s
}
type Name = Name.Type
object Email {
opaque type Type <: String = String
def apply(s: String): Type = s
}
type Email = Email.Type
}
object using {
import types.{Name, Email}
@main
def test (): Unit = {
def foo(str: String): Unit = {
println(str)
}
def bar(n: Name, e: Email): Unit = {
println(s"$n @ $e")
}
val name = Name("JDoe")
val email = Email("[email protected]")
//foo(name)
// Found: (name : types.Name.Type)
// Required: String
//foo(email)
// Found: (email : types.Email.Type)
// Required: String
foo(name)
foo(email)
bar(name, email)
println(name.toLowerCase())
//bar(name.toLowerCase(), email)
// Found: String
// Required: types.Name
bar(Name(name.toLowerCase()), email)
//bar(email, name)
// Found: (email : types.Email.Type)
// Required: types.Name
// Found: (name : types.Name.Type)
// Required: types.Email
}
}