scalanewtyperefinement-typerefined

How to ensure type safety with Scala's Refined library when using the same predicate for refinement


I am new to scala and the refined library but I am trying to create two refined types based on UUIDs.

In order to do so, I did this (Note: Uuid in this case comes from eu.timepit.refined.string.Uuid):

type UuidPredicate = Uuid

type UuidA = String Refined UuidPredicate
type UuidB = String Refined UuidPredicate

However, it appears as though this only creates aliases, and therefore there is no type safety.

So if I had a constructor like Product(a UuidA, b UuidB) and proceeded to do something like this:

val myUuid: UuidA = "9f9ef0c6-b6f8-11ea-b3de-0242ac130004"
val Product = Product(myUuid, myUuid)

It would compile and run properly. Is there anyway to ensure this is not the case? If a variable is created as one type, how can I make it so it only be used as that particular refined type, even though the types are fundamentally the same?


Solution

  • The simplest is to introduce different data types

    case class UuidA(value: String Refined UuidPredicate)
    case class UuidB(value: String Refined UuidPredicate)
    

    You can't make UuidA, UuidB extend AnyVal because Refined already extends AnyVal and Scala doesn't allow nested value classes.

    If you prefer to avoid runtime overhead of wrapping with UuidA, UuidB you can try @newtype as @LuisMiguelMejíaSuárez adviced

    import io.estatico.newtype.macros.newtype
    
    @newtype case class UuidA(value: String Refined UuidPredicate)
    @newtype case class UuidB(value: String Refined UuidPredicate)
    

    Or try to add more tags

    import eu.timepit.refined.api.Refined
    import eu.timepit.refined.string.Uuid
    import eu.timepit.refined.auto._
    import shapeless.tag
    import shapeless.tag.@@
    
    type UuidPredicate = Uuid
    type UuidString = Refined[String, UuidPredicate]
    type TagA
    type TagB
    type UuidA = UuidString @@ TagA
    type UuidB = UuidString @@ TagB
    
    case class Product(a: UuidA, b: UuidB)
    val myUuid: UuidA = tag[TagA][UuidString]("9f9ef0c6-b6f8-11ea-b3de-0242ac130004")
    //  val product = Product(myUuid, myUuid) // doesn't compile
    val myUuid1: UuidB = tag[TagB][UuidString]("9f9ef0c6-b6f8-11ea-b3de-0242ac130004")
    val product1 = Product(myUuid, myUuid1) // compiles