Creating New Types

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

RefinedType

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

type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
val temperature = Temperature(15) //Compiles
println(temperature) //15

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

Runtime refinement

RefinedType supports all refinement methods provided by Iron:

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:

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

Note: just like IronType, [[RefinedType|io.github.iltotore.iron.RefinedType] compiles to its base type.

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 = Temperature.applyUnsafe(runtimeValue)

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.

type FirstName = FirstName.T
object FirstName extends RefinedType[String, Pure]
val firstName = FirstName("whatever")

Opacity

A newtype is opaque. Therefore, it is only known in its definition file. It is not considered to be a type alias outside of it:

type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
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:

type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]

type Moisture = Moisture.T
object Moisture extends RefinedType[Double, Positive]
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 RefinedType:

type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]
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.*

type FirstName = FirstNamle
object FirstName extends RefinedType[String, ForAll[Letter]]

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:

type Temperature = Temperature.T
object Temperature extends RefinedType[Double, Positive]

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

inline given[T](using mirror: RefinedType.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 RefinedType.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.