Refined Type - Custom Type
What is a Custom Refined Type?
A custom Refined type is useful when your domain rule is not covered by a pre-defined type.
Use it when you want:
- a domain-specific name (e.g.
Month,OrderId,Username) - reusable validation logic in one place
- compile-time + runtime validation with one definition
Import
import refined4s.*
Define a Refined Type
type RefinedTypeName = RefinedTypeName.Type
object RefinedTypeName extends Refined[ActualType] {
override inline def invalidReason(a: ActualType): String =
expectedMessage("something with blah blah")
override inline def predicate(a: ActualType): Boolean =
// validation logic here
}
Example:
type MyString = MyString.Type
object MyString extends Refined[String] {
override inline def invalidReason(a: String): String =
expectedMessage("a non-empty String")
override inline def predicate(a: String): Boolean =
a != ""
}
Create Values
Given the following Refined type:
type Month = Month.Type
object Month extends Refined[Int] {
override inline def invalidReason(a: Int): String =
expectedMessage("Int between 1 and 12 (1 - 12)")
override inline def predicate(a: Int): Boolean =
a >= 1 && a <= 12
}
Compile-time Validation (apply)
Use apply when the input is known at compile-time.
Valid cases:
Month(1)
// res1: Type = 1
Month(12)
// res2: Type = 12
Invalid cases:
Month(0)
// Month(0)
// ^^^^^^^^
// Invalid value: [0]. It must be Int between 1 and 12 (1 - 12)
Month(13)
// Month(13)
// ^^^^^^^^^
// Invalid value: [13]. It must be Int between 1 and 12 (1 - 12)
Runtime Validation (from)
Use from when the input comes from runtime sources (request, DB, config, etc.).
val monthInput1 = 1
// monthInput1: Int = 1
val monthInput2 = 12
// monthInput2: Int = 12
val validMonthResult1: Either[String, Month] = Month.from(monthInput1)
// validMonthResult1: Either[String, Month] = Right(value = 1)
val validMonthResult2: Either[String, Month] = Month.from(monthInput2)
// validMonthResult2: Either[String, Month] = Right(value = 12)
validMonthResult1
// res3: Either[String, Month] = Right(value = 1)
validMonthResult2
// res4: Either[String, Month] = Right(value = 12)
val invalidMonthInput1 = 0
// invalidMonthInput1: Int = 0
val invalidMonthInput2 = 13
// invalidMonthInput2: Int = 13
Month.from(invalidMonthInput1)
// res5: Either[String, Type] = Left(
// value = "Invalid value: [0]. It must be Int between 1 and 12 (1 - 12)."
// )
Month.from(invalidMonthInput2)
// res6: Either[String, Type] = Left(
// value = "Invalid value: [13]. It must be Int between 1 and 12 (1 - 12)."
// )
Functional Runtime Handling
def renderMonth(input: Int): String =
Month.from(input).fold(
error => s"Invalid month input: $error",
month => s"Validated month: ${month.value}"
)
renderMonth(5)
// res7: String = "Validated month: 5"
renderMonth(13)
// res8: String = "Invalid month input: Invalid value: [13]. It must be Int between 1 and 12 (1 - 12)."
Runtime Unsafe Validation (unsafeFrom)
danger
unsafeFrom may throw an exception if the input value is invalid. Prefer from in most cases.
Month.unsafeFrom(invalidMonthInput2)
// java.lang.IllegalArgumentException: Invalid value: [13]. It must be Int between 1 and 12 (1 - 12).
// at refined4s.RefinedBase.unsafeFrom$$anonfun$1(RefinedBase.scala:27)
// at scala.util.Either.fold(Either.scala:198)
// at refined4s.RefinedBase.unsafeFrom(RefinedBase.scala:27)
// at refined4s.RefinedBase.unsafeFrom$(RefinedBase.scala:7)
// at repl.MdocSession$MdocApp0$Month$.unsafeFrom(custom-type.md:34)
// at repl.MdocSession$MdocApp0$.$init$$$anonfun$1(custom-type.md:125)
Get Actual Value
Use .value to get the underlying value.
val month = Month(1)
// month: Type = 1
month.value
// res9: Int = 1
Pattern Matching
Refined provides unapply, so you can pattern match directly.
month match {
case Month(value) =>
println(s"Pattern matched value: $value")
}
// Pattern matched value: 1
Guidelines
- Keep
predicatepure and deterministic. - Keep
invalidReasonspecific and user-facing. - Prefer
fromat runtime boundaries for total, functional flows. - Use
applyfor literal constants where compile-time checking is possible.