kotlinintermediate

Value Classes — Zero-Cost Wrappers

Create type-safe wrappers with value classes: no runtime overhead, domain identifiers, and units.

kotlin
@JvmInline
value class UserId(val value: Long) {
    init {
        require(value > 0) { "UserId must be positive" }
    }
}

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email: $value" }
    }
    val domain: String get() = value.substringAfter("@")
    val local: String get() = value.substringBefore("@")
    fun normalized() = Email(value.lowercase().trim())
}

@JvmInline
value class Password(private val value: String) {
    init {
        require(value.length >= 8) { "Password must be at least 8 characters" }
    }
    // Don't expose in toString!
    override fun toString() = "Password(***)"
    fun verify(input: String) = value == input
}

// Units of measure
@JvmInline
value class Meters(val value: Double) {
    operator fun plus(other: Meters) = Meters(value + other.value)
    operator fun minus(other: Meters) = Meters(value - other.value)
    operator fun times(scalar: Double) = Meters(value * scalar)
    fun toKilometers() = Kilometers(value / 1000.0)
    fun toFeet() = Feet(value * 3.28084)
    override fun toString() = "${"%.2f".format(value)}m"
}

@JvmInline
value class Kilometers(val value: Double) {
    fun toMeters() = Meters(value * 1000.0)
    override fun toString() = "${"%.2f".format(value)}km"
}

@JvmInline
value class Feet(val value: Double) {
    fun toMeters() = Meters(value / 3.28084)
    override fun toString() = "${"%.2f".format(value)}ft"
}

// Money with currency safety
@JvmInline
value class USD(val cents: Long) {
    constructor(dollars: Int, cents: Int = 0) : this(dollars * 100L + cents)
    val dollars get() = cents / 100
    val remainingCents get() = cents % 100
    operator fun plus(other: USD) = USD(cents + other.cents)
    operator fun minus(other: USD) = USD(cents - other.cents)
    operator fun times(quantity: Int) = USD(cents * quantity)
    override fun toString() = "\$${dollars}.${remainingCents.toString().padStart(2, '0')}"
}

// Type-safe IDs prevent mixing
@JvmInline
value class OrderId(val value: String)
@JvmInline
value class ProductId(val value: String)

data class OrderItem(
    val orderId: OrderId,
    val productId: ProductId,
    val quantity: Int,
    val price: USD
)

// Function that won't accept wrong ID type
fun findOrder(id: OrderId): String = "Order: ${id.value}"
fun findProduct(id: ProductId): String = "Product: ${id.value}"
// findOrder(ProductId("p-1")) ← compile error!

fun main() {
    // Domain IDs
    val userId = UserId(42)
    val email = Email("Alice@Example.com")
    val password = Password("secret123")

    println("User: $userId")
    println("Email: ${email.normalized().value}")
    println("Domain: ${email.domain}")
    println("Password: $password") // Password(***)
    println("Verify: ${password.verify("secret123")}")

    // Units
    val distance = Meters(1500.0)
    println("\nDistance: $distance")
    println("In km: ${distance.toKilometers()}")
    println("In feet: ${distance.toFeet()}")
    println("Double: ${distance * 2.0}")

    // Money
    val price = USD(29, 99)
    val tax = USD(2, 40)
    val total = price + tax
    println("\nPrice: $price")
    println("Tax: $tax")
    println("Total: $total")
    println("3x: ${price * 3}")

    // Type-safe IDs
    val orderId = OrderId("ord-123")
    val productId = ProductId("prod-456")
    println("\n${findOrder(orderId)}")
    println(findProduct(productId))

    val item = OrderItem(orderId, productId, 2, price)
    println("Item: $item")
}

Use Cases

  • Type-safe domain identifiers
  • Units of measure without overhead
  • Preventing primitive type confusion

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.