Creating New Types

You can create no-overhead new types like scala-newtype in Scala 2 with Iron.

RefinedTypeOps

Iron provides a convenient trait called RefinedTypeOps to easily add smart constructors to your type:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

val temperature = Temperature(15) //Compiles
println(temperature) //15

val positive: Double :| Positive = 15
val tempFromIron = Temperature(positive) //Compiles too

For transparent type aliases, it is possible to use the RefinedTypeOps.Transparent alias to avoid boilerplate.

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]

Runtime refinement

RefinedTypeOps supports all refinement methods provided by Iron:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]

val unsafeRuntime: Temperature = Temperature.applyUnsafe(15)
val option: Option[Temperature] = Temperature.option(15)
val either: Either[String, Temperature] = Temperature.either(15)

Constructors for other modules exist:

import zio.prelude.Validation

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive
import io.github.iltotore.iron.zio.*

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]

val zioValidation: Validation[String, Temperature] = Temperature.validation(15)

Note: all these constructors are inline. They don't bring any overhead:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps.Transparent[Temperature]

val temperature: Temperature = Temperature(15)

val runtimeValue: Double = ???
val unsafeRuntime: Temperature = Temperature.applyUnsafe(runtimeValue)

compiles to

val temperature: Double = 15

val runtimeValue: Double = ???
val unsafeRuntime: Double =
  if runtimeValue > 0 then runtimeValue
  else throw new IllegalArgumentException("...")

New type with no constraint

You can create a new type without restriction by using the Pure constraint. Pure is an alias for True, a constraint that is always satisfied.

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Pure

type FirstName = String :| Pure
object FirstName extends RefinedTypeOps.Transparent[FirstName]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.any.Pure

type FirstName = String :| Pure
object FirstName extends RefinedTypeOps.Transparent[FirstName]

val firstName = FirstName("whatever")

Opaque new types

The aliased type of an opaque type is only known in its definition file. It is not considered like a type alias outside of it:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

val x: Double :| Positive = 5
val temperature: Temperature = x //Error: Temperature expected, got Double :| Positive

Such encapsulation is especially useful to avoid mixing different domain types with the same refinement:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

opaque type Moisture = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Moisture]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

opaque type Moisture = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Moisture]

case class Info(temperature: Temperature, moisture: Moisture)

val temperature: Temperature = ???
val moisture: Moisture = ???

Info(moisture, temperature) //Compile-time error

Therefore, it also forces the user to convert the value explicitly, for example using a smart constructor from RefinedTypeOps:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

val value: Double :| Positive = ???

val a: Temperature = value //Compile-time error
val b: Temperature = Temperature(value) //OK

Inheriting base type

Assuming the following new type:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*

opaque type FirstName = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]

We cannot use java.lang.String's methods neither pass FirstName as a String without using the value extension method. In Scala 3, opaque types can be a subtype of their underlying type:

opaque type Foo <: String = String
object Foo:
  def apply(value: String): Foo = value
opaque type Foo <: String = String
object Foo:
  def apply(value: String): Foo = value

val x = Foo("abcd")
x.toUpperCase //"ABCD"

Therefore, you can combine it with RefinedTypeOps:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*

opaque type FirstName <: String :| ForAll[Letter] = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]
import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.all.*

opaque type FirstName <: String :| ForAll[Letter] = String :| ForAll[Letter]
object FirstName extends RefinedTypeOps[String, ForAll[Letter], FirstName]

val x = FirstName("Raphael")
x.toUpperCase //"RAPHAEL"

Typeclass derivation

Usually, transparent type aliases do not need a special handling for typeclass derivation as they can use the given instances for IronType. However, this is not the case for opaque type aliases, for instance:

import io.github.iltotore.iron.*
import io.github.iltotore.iron.constraint.numeric.Positive

opaque type Temperature = Double :| Positive
object Temperature extends RefinedTypeOps[Double, Positive, Temperature]

To support such type, you can use the RefinedTypeOps.Mirror provided by each RefinedTypeOps. It works the same as Scala 3's Mirror. Here is an example from the ZIO JSON module:

import zio.json.*
import io.github.iltotore.iron.*

inline given[T](using mirror: RefinedTypeOps.Mirror[T], ev: JsonDecoder[mirror.IronType]): JsonDecoder[T] =
  ev.asInstanceOf[JsonDecoder[T]]

In this example, given a new type T (like Temperature defined above), an implicit instance of JsonEncoder for its underlying IronType (e.g Double :| Positive) is got and returned.

The types provided by RefinedTypeOps.Mirror are:

  • BaseType: the base (unrefined) type of the mirrored type.
  • ConstraintType: the constraint type of the mirrored new type.
  • IronType: an alias for BaseType :| ConstraintType
  • FinalType: the underlying type of the mirrored new type. Equivalent to its IronType if the alias is not opaque.