Skip to main content
Version: v1.16.0

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

  1. Keep predicate pure and deterministic.
  2. Keep invalidReason specific and user-facing.
  3. Prefer from at runtime boundaries for total, functional flows.
  4. Use apply for literal constants where compile-time checking is possible.