In Scala, it’s better to create a single ID wrapper type with a phantom type variable and reuse that everywhere. Here’s what I mean:
import java.util.UUID
import scala.util.Try
/* We don't need pattern matching, so make it a normal class. But make
the `toUUID` member field public so we can convert the ID back to its
contained UUID at any time. */
class ID[Tag] private (val toUUID: UUID) extends AnyVal
object ID {
def apply[Tag](): ID[Tag] = new ID(UUID.randomUUID())
def apply[Tag](string: String): Try[ID[Tag]] =
Try(UUID fromString string) map { uuid =>
new ID(uuid)
}
}
Usage (paste in REPL):
case class Person(id: ID[Person], name: String)
val bob = Person(ID(), "Bob")
Explanation:
You’ll notice that the type parameter Tag is actually never used in the type definition. This pattern is called a phantom type and is used to distinguish between values which have the same underlying type but a different ‘tag’ (the phantom type).
Now you can reuse the same ID[Tag] class for many different domain types and have them all be safe and distinct from each other at the type level, without repetition.
P.S., the pattern case class Foo private (something: Int) extends AnyVal does not work like you think it does: because Scala auto-generates a Foo.apply(something: Int): Foo method, users can still create instances outside of your control. That’s also another reason I used a normal class instead of a case class. In general it’s a good idea to use case classes carefully–they auto-generate a lot of things ( https://stackoverflow.com/a/30530447/20371 ) you may not need.