Defining Newtypes

You can define newtypes with or without validation. Newtypes are a way to create type-safe wrappers around existing types, ensuring type safety and domain correctness at compile time.

With Validation

When you need to ensure that values conform to specific rules, use Newtype.ValidatedOf. The underlying type is inferred from the validator rules:

import yantl.*

object Age extends Newtype.ValidatedOf(Validator.of(ValidatorRule.minValue(0L)))
type Age = Age.Type

This creates an Age type that:

Creating validated instances:

// Safe creation
Age.make(25)
// res0: Either[Vector[TError], Type] = Right(value = 25L)
Age.make(-5)
// res1: Either[Vector[TError], Type] = Left(
//   value = Vector(SmallerThan(minimum = 0L, actual = -5L))
// )

// Creation with multiple string error messages
Age.makeAsStrings(-5)
// res2: Either[Vector[String], Type] = Left(
//   value = Vector("Cannot be smaller than 0, was -5.")
// )

// Creation with a single string error message
Age.makeAsString(-5)
// res3: Either[String, Type] = Left(
//   value = "Cannot be smaller than 0, was -5."
// )

// Performs no checking, allowing invalid values to be created
Age.makeUnsafe(-5)
// res4: Type = -5L

For statically known values, you can use makeOrThrow to throw an exception if the value is invalid:

Age.makeOrThrow(-5)
// java.lang.IllegalArgumentException: Cannot be smaller than 0, was -5.
// 	at yantl.Newtype.makeOrThrow(Newtype.scala:59)
// 	at yantl.Newtype.makeOrThrow$(Newtype.scala:3)
// 	at repl.MdocSession$MdocApp$Age$.makeOrThrow(001_defining_newtypes.md:10)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$1(001_defining_newtypes.md:46)

Without Validation

When you just need type safety without validation rules:

object Name extends Newtype.WithoutValidationOf[String]
type Name = Name.Type

This creates a Name type that:

Creating unvalidated instances:

Name("John")
// res5: Type = "John"

Intermediatary Newtypes

Sometimes (for example, in a framework) you want to define a newtype that has some functionality, but it is not the final type. For example, a newtype helper for non-empty strings:

/** A newtype wrapping a [[String]]. */
trait NewtypeString extends Newtype.Of[String] {
  given CanEqual[Type, Type] = CanEqual.derived

  given Ordering[Type] = Ordering.by(unwrap)
}

/** A newtype wrapping a non-empty [[String]] without surrounding whitespace. */
trait NewtypeNonEmptyString extends NewtypeString {
  // Unfortunately you have to repeat the type here
  type TError = ValidatorRule.HadSurroundingWhitespace | ValidatorRule.WasBlank

  override val validator = NewtypeNonEmptyString.validator
}
object NewtypeNonEmptyString {
  val validator = Validator.of(
    ValidatorRule.nonBlankString,
    ValidatorRule.withoutSurroundingWhitespace,
  )
}

Then, end-user code can use NewtypeNonEmptyString to get the Ordering instance for free:

object ForumTopic extends NewtypeNonEmptyString
type ForumTopic = ForumTopic.Type

ForumTopic.make("What are newtypes?")
// res6: Either[Vector[TError], Type] = Right(value = "What are newtypes?")

ForumTopic.make("")
// res7: Either[Vector[TError], Type] = Left(
//   value = Vector(WasBlank(value = ""))
// )

Or they can refine the type even further:

object ForumTopicStrict extends NewtypeString {
  type TError =
    ValidatorRule.HadSurroundingWhitespace | 
      ValidatorRule.WasBlank | 
      ValidatorRule.UnderMinLength[String]

  override val validator = 
    NewtypeNonEmptyString.validator and Validator.of(ValidatorRule.minLength(10))
}
type ForumTopicStrict = ForumTopicStrict.Type

ForumTopicStrict.make("What are newtypes?")
// res8: Either[Vector[TError], Type] = Right(value = "What are newtypes?")

ForumTopicStrict.make("newtypes?")
// res9: Either[Vector[TError], Type] = Left(
//   value = Vector(
//     UnderMinLength(minLength = 10, actualLength = 9, actual = "newtypes?")
//   )
// )

Best Practices

  1. Always define both the object and type alias:

    object MyType extends Newtype.WithoutValidationOf[String]
    type MyType = MyType.Type  // Allows using `MyType` as your type in the codebase
  2. Use validation when your type has invariants that must be maintained

  3. Use WithoutValidation when you just need type safety

  4. Use makeOrThrow only when you are certain the value is valid, like in statically known values