scalaintermediate
Refined and Constrained Types
Create constrained types with validation: non-empty strings, bounded numbers, and validated domain types.
scalaPress ⌘/Ctrl + Shift + C to copy
// Opaque types with validation
object Types:
// Non-empty string
opaque type NonEmptyString = String
object NonEmptyString:
def apply(s: String): Either[String, NonEmptyString] =
if s.nonEmpty then Right(s)
else Left("String must not be empty")
def unsafe(s: String): NonEmptyString =
require(s.nonEmpty, "String must not be empty")
s
extension (s: NonEmptyString)
def value: String = s
def length: Int = s.length
// Positive integer
opaque type PosInt = Int
object PosInt:
def apply(n: Int): Either[String, PosInt] =
if n > 0 then Right(n)
else Left(s"Must be positive: $n")
extension (n: PosInt)
def value: Int = n
def +(other: PosInt): PosInt = (n: Int) + (other: Int)
// Bounded int
opaque type Percentage = Int
object Percentage:
def apply(n: Int): Either[String, Percentage] =
if n >= 0 && n <= 100 then Right(n)
else Left(s"Must be 0-100: $n")
extension (p: Percentage)
def value: Int = p
def asDouble: Double = p / 100.0
// Email
opaque type Email = String
object Email:
private val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$""".r
def apply(s: String): Either[String, Email] =
if emailRegex.matches(s) then Right(s)
else Left(s"Invalid email: $s")
extension (e: Email)
def value: String = e
def domain: String = e.split("@")(1)
def local: String = e.split("@")(0)
// Port number
opaque type Port = Int
object Port:
def apply(n: Int): Either[String, Port] =
if n >= 1 && n <= 65535 then Right(n)
else Left(s"Invalid port: $n")
val HTTP: Port = 80
val HTTPS: Port = 443
extension (p: Port)
def value: Int = p
def isPrivileged: Boolean = p < 1024
import Types.*
// Domain model using refined types
case class ServerConfig(
host: NonEmptyString,
port: Port,
maxRetries: PosInt
)
case class UserProfile(
name: NonEmptyString,
email: Email,
completeness: Percentage
)
// Smart constructor
def createProfile(
name: String, email: String, completeness: Int
): Either[String, UserProfile] =
for
n <- NonEmptyString(name)
e <- Email(email)
c <- Percentage(completeness)
yield UserProfile(n, e, c)
@main def run(): Unit =
// Valid
println(NonEmptyString("hello").map(_.value))
println(PosInt(42).map(_.value))
println(Email("alice@test.com").map(e => s"${e.local} @ ${e.domain}"))
println(Percentage(75).map(p => f"${p.asDouble}%.0f%%"))
println(Port(8080).map(p => s"port ${p.value} privileged=${p.isPrivileged}"))
// Invalid
println(NonEmptyString(""))
println(PosInt(-1))
println(Email("not-email"))
println(Percentage(150))
println(Port(99999))
// Domain model
println(s"\nValid profile: ${createProfile("Alice", "alice@test.com", 85)}")
println(s"Invalid profile: ${createProfile("", "bad", 150)}")
// Compose
val config = for
host <- NonEmptyString("localhost")
port <- Port(8080)
retries <- PosInt(3)
yield ServerConfig(host, port, retries)
println(s"Config: $config")Use Cases
- Domain type validation
- Compile-time type safety
- Input validation at boundaries
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
scalaintermediate
Error Accumulation with Validated
Accumulate multiple validation errors instead of failing fast using Cats Validated and custom validators.
Best for: Form validation with all errors
#scala#validation
scalabeginner
Type Aliases and Abstract Types
Define type aliases, abstract types, and type members for cleaner and more expressive APIs.
Best for: Simplifying complex type signatures
#scala#types
scalabeginner
Type Conversions and Safe Casting
Convert between types safely: numeric conversions, asInstanceOf alternatives, and pattern-based casting.
Best for: Safe data type conversions
#scala#types
scalabeginner
Scala Hello World Application
Create a basic Scala application with main method, string interpolation, and val/var basics.
Best for: Getting started with Scala
#scala#basics