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:
- Wraps a
Long
value - Ensures the value is non-negative
- Provides type safety (you can't accidentally use a raw
Long
where anAge
is expected)
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:
- Wraps a
String
value - Provides type safety
- Has no validation rules
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
-
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
-
Use validation when your type has invariants that must be maintained
-
Use
WithoutValidation
when you just need type safety -
Use
makeOrThrow
only when you are certain the value is valid, like in statically known values