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.*
case object Age extends Newtype.ValidatedOf(Validator.of(ValidatorRule.minValue(0L)))
type Age = Age.Type
        This creates an Age type that:
- Wraps a 
Longvalue - Ensures the value is non-negative
 - Provides type safety (you can't accidentally use a raw 
Longwhere anAgeis 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))
// )
// With errors as strings.
// You need to define how to convert the error messages to strings first.
given [A]: AsString[A] = AsString.fromToString
// Creation with multiple string error messages
Age.make.asStrings(-5)
// res2: Either[Vector[String], Type] = Left(
//   value = Vector("Cannot be smaller than 0, was -5.")
// )
// Creation with a single string error message
Age.make.asString(-5)
// res3: Either[String, Type] = Left(
//   value = "Cannot be smaller than 0, was -5."
// )
// Performs no checking, allowing invalid values to be created
Age.make.unsafe(-5)
// res4: Type = -5L
        For statically known values, you can use makeOrThrow to throw an exception if the value is invalid:
Age.make.orThrow(-5)
// java.lang.IllegalArgumentException: Cannot be smaller than 0, was -5.
// 	at yantl.Make.orThrow(Make.scala:48)
// 	at yantl.Make.orThrow$(Make.scala:15)
// 	at yantl.INewtype$make$.orThrow(Newtype.scala:31)
// 	at repl.MdocSession$MdocApp.$init$$$anonfun$1(001_defining_newtypes.md:49)
        
        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 
Stringvalue - 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 validate = 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:
case 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:
case object ForumTopicStrict extends NewtypeString {
  type TError =
    ValidatorRule.HadSurroundingWhitespace | 
      ValidatorRule.WasBlank | 
      ValidatorRule.UnderMinLength[String]
  override val validate = 
    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?")
//   )
// )
        
        Chained Newtypes
You can have newtypes that are based off other newtypes. For example, you can define an Email newtype and then more 
        specific GoogleMailEmail newtype that refines Email even more.
case class NotAnEmail(email: String)
case object Email extends Newtype.ValidatedOf(Validator.of(
  ValidatorRule.of { (email: String) =>
    if (email.contains("@")) None else Some(NotAnEmail(email))
  }
))
type Email = Email.Type
case class NotAGoogleMail(email: Email)
case object GoogleMailEmail extends Newtype.ValidatedOf(Validator.of(
  ValidatorRule.of { (email: Email) =>
    if (Email.unwrap(email).endsWith("@gmail.com")) None else Some(NotAGoogleMail(email))
  }
))
val ChainedGoogleMailEmail = Email.compose(GoogleMailEmail)
// ChainedGoogleMailEmail: INewtype {
  type TUnderlying >: TUnderlying <: TUnderlying
  type TError >: TError | TError <: TError | TError
  type Type >: Type <: Type
} = ComposedNewtype(Email -> GoogleMailEmail)
type ChainedGoogleMailEmail = ChainedGoogleMailEmail.Type
val notAnEmail = ChainedGoogleMailEmail.make("foo")
// notAnEmail: Either[Vector[TError], Type] = Left(
//   value = Vector(NotAnEmail(email = "foo"))
// )
val outlook = ChainedGoogleMailEmail.make("foo@outlook.com")
// outlook: Either[Vector[TError], Type] = Left(
//   value = Vector(NotAGoogleMail(email = "foo@outlook.com"))
// )
val gmail = ChainedGoogleMailEmail.make("foo@gmail.com")
// gmail: Either[Vector[TError], Type] = Right(value = "foo@gmail.com")
gmail == Email.make("foo@gmail.com").flatMap(GoogleMailEmail.make(_))
// res10: Boolean = true
        
        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
WithoutValidationwhen you just need type safety - 
            
Use
makeOrThrowonly when you are certain the value is valid, like in statically known values