Skip to content

Latest commit

 

History

History
900 lines (680 loc) · 20 KB

File metadata and controls

900 lines (680 loc) · 20 KB
id title
typeid
TypeId

TypeId

TypeId[A] represents the identity of a type or type constructor at runtime. It provides rich type identity information including the type's name, owner (package/class/object), type parameters, classification (nominal, alias, or opaque), parent types, and annotations.

Overview

TypeId is fundamental to ZIO Blocks' schema system, enabling:

  • Type identification - Uniquely identify types across serialization boundaries
  • Subtype checking - Determine inheritance relationships at runtime
  • Type normalization - Resolve type aliases to their underlying types
  • Schema derivation - Automatically derive schemas for user-defined types
import zio.blocks.typeid._

// Derive TypeId for your types
case class Person(name: String, age: Int)
val personId: TypeId[Person] = TypeId.of[Person]

// Access type information
personId.name       // "Person"
personId.fullName   // "com.example.Person"
personId.isCaseClass // true

// Use predefined TypeIds
TypeId.int.fullName      // "scala.Int"
TypeId.string.fullName   // "java.lang.String"
TypeId.list.arity        // 1 (type constructor)

Installation

TypeId is included in the zio-blocks-typeid module. Add it to your build:

libraryDependencies += "dev.zio" %% "zio-blocks-typeid" % "<version>"

Cross-platform support: TypeId works on JVM and Scala.js.

Creating TypeIds

Automatic Derivation

The simplest way to get a TypeId is via macro derivation:

import zio.blocks.typeid._

case class User(id: Long, email: String)

// Scala 3
val userId: TypeId[User] = TypeId.of[User]

// Scala 2
val userId: TypeId[User] = TypeId.of[User]

// Or use implicit derivation
val userId: TypeId[User] = implicitly[TypeId[User]]

The macro extracts complete type information including:

  • Type name and owner
  • Type parameters and variance
  • Parent types (for sealed traits and enums)
  • Whether it's a case class, sealed trait, enum, etc.

Manual Construction

For manual type registration or testing, use smart constructors:

// Nominal types (classes, traits, objects)
val myTypeId = TypeId.nominal[MyType](
  name = "MyType",
  owner = Owner.fromPackagePath("com.example"),
  defKind = TypeDefKind.Class(isCase = true)
)

// Type aliases
val aliasId = TypeId.alias[Age](
  name = "Age",
  owner = Owner.fromPackagePath("com.example"),
  aliased = TypeRepr.Ref(TypeId.int)
)

// Opaque types (Scala 3)
val emailId = TypeId.opaque[Email](
  name = "Email",
  owner = Owner.fromPackagePath("com.example"),
  representation = TypeRepr.Ref(TypeId.string)
)

Applied Types

Create applied types (type constructors with arguments):

// List[Int]
val listIntId = TypeId.applied[List[Int]](
  TypeId.list,
  TypeRepr.Ref(TypeId.int)
)

// Map[String, Int]
val mapId = TypeId.applied[Map[String, Int]](
  TypeId.map,
  TypeRepr.Ref(TypeId.string),
  TypeRepr.Ref(TypeId.int)
)

TypeId Properties

Basic Properties

val id = TypeId.of[Person]

id.name           // "Person" - simple name
id.fullName       // "com.example.Person" - fully qualified
id.owner          // Owner representing the package/enclosing type
id.arity          // 0 for proper types, n for type constructors
id.typeParams     // List of TypeParam for type constructors
id.typeArgs       // List of TypeRepr for applied types

Type Classification

id.isClass        // true for classes
id.isTrait        // true for traits
id.isObject       // true for singleton objects
id.isEnum         // true for Scala 3 enums
id.isCaseClass    // true for case classes
id.isValueClass   // true for value classes (extends AnyVal)
id.isSealed       // true for sealed traits
id.isAlias        // true for type aliases
id.isOpaque       // true for opaque types
id.isAbstract     // true for abstract type members

id.isProperType      // arity == 0
id.isTypeConstructor // arity > 0
id.isApplied         // has type arguments

Common Type Checks

id.isTuple   // scala.TupleN
id.isProduct // scala.ProductN
id.isSum     // Either or Option
id.isEither  // scala.util.Either
id.isOption  // scala.Option

Subtype Relationships

sealed trait Animal
case class Dog(name: String) extends Animal

val dogId = TypeId.of[Dog]
val animalId = TypeId.of[Animal]

dogId.isSubtypeOf(animalId)    // true
animalId.isSupertypeOf(dogId)  // true
dogId.isEquivalentTo(dogId)    // true

Subtype checking handles:

  • Direct inheritance
  • Enum cases and their parent enums
  • Sealed trait subtypes
  • Transitive inheritance
  • Variance-aware subtyping for applied types

Pattern Matching

TypeId provides extractors for pattern matching:

typeId match {
  case TypeId.Nominal(name, owner, params, defKind, parents) =>
    // Regular types
    
  case TypeId.Alias(name, owner, params, aliased) =>
    // Type aliases - aliased is the underlying TypeRepr
    
  case TypeId.Opaque(name, owner, params, repr, bounds) =>
    // Opaque types - repr is the representation type
    
  case TypeId.Sealed(name) =>
    // Sealed traits
    
  case TypeId.Enum(name, owner) =>
    // Scala 3 enums
}

TypeRepr

TypeRepr represents type expressions in the Scala type system. While TypeId identifies a type definition, TypeRepr represents how types are used in expressions.

Basic Type References

// Reference to a named type
TypeRepr.Ref(TypeId.int)            // Int
TypeRepr.Ref(TypeId.string)         // String

// Reference to a type parameter
TypeRepr.ParamRef(TypeParam.A)      // A
TypeRepr.ParamRef(param, depth = 1) // nested binder reference

Applied Types

// List[Int]
TypeRepr.Applied(
  TypeRepr.Ref(TypeId.list),
  List(TypeRepr.Ref(TypeId.int))
)

// Map[String, Int]
TypeRepr.Applied(
  TypeRepr.Ref(TypeId.map),
  List(TypeRepr.Ref(TypeId.string), TypeRepr.Ref(TypeId.int))
)

Compound Types

// Intersection: A & B (Scala 3) or A with B (Scala 2)
TypeRepr.Intersection(List(typeA, typeB))

// Union: A | B (Scala 3 only)
TypeRepr.Union(List(typeA, typeB))

// Convenience constructors handle edge cases
TypeRepr.intersection(List(typeA))      // returns typeA (not Intersection)
TypeRepr.intersection(Nil)              // returns AnyType
TypeRepr.union(List(typeA))             // returns typeA
TypeRepr.union(Nil)                     // returns NothingType

Function Types

// A => B
TypeRepr.Function(List(typeA), typeB)

// (A, B) => C
TypeRepr.Function(List(typeA, typeB), typeC)

// (A, B) ?=> C (context function, Scala 3)
TypeRepr.ContextFunction(List(typeA, typeB), typeC)

Tuple Types

// (A, B, C)
TypeRepr.Tuple(List(
  TupleElement(None, typeA),
  TupleElement(None, typeB),
  TupleElement(None, typeC)
))

// Named tuples (Scala 3.5+): (name: String, age: Int)
TypeRepr.Tuple(List(
  TupleElement(Some("name"), TypeRepr.Ref(TypeId.string)),
  TupleElement(Some("age"), TypeRepr.Ref(TypeId.int))
))

// Convenience for unnamed tuples
TypeRepr.tuple(List(typeA, typeB, typeC))

Structural Types

// { def foo: Int }
TypeRepr.Structural(
  parents = Nil,
  members = List(
    Member.Def("foo", Nil, Nil, TypeRepr.Ref(TypeId.int))
  )
)

// AnyRef { type T; val x: T }
TypeRepr.Structural(
  parents = List(TypeRepr.Ref(anyRefId)),
  members = List(
    Member.TypeMember("T"),
    Member.Val("x", TypeRepr.ParamRef(paramT))
  )
)

Path-Dependent and Singleton Types

// x.type (singleton type)
TypeRepr.Singleton(TermPath.fromOwner(owner, "x"))

// this.type
TypeRepr.ThisType(owner)

// Outer#Inner (type projection)
TypeRepr.TypeProjection(outerType, "Inner")

// qualifier.Member (type selection)
TypeRepr.TypeSelect(qualifierType, "Member")

Special Types

TypeRepr.AnyType       // Any
TypeRepr.NothingType   // Nothing
TypeRepr.NullType      // Null
TypeRepr.UnitType      // Unit
TypeRepr.AnyKindType   // AnyKind (for kind-polymorphic contexts)

Constant/Literal Types

TypeRepr.Constant.IntConst(42)         // 42 (literal type)
TypeRepr.Constant.StringConst("foo")   // "foo"
TypeRepr.Constant.BooleanConst(true)   // true
TypeRepr.Constant.ClassOfConst(tpe)    // classOf[T]

Type Lambdas (Scala 3)

// [X] =>> F[X]
TypeRepr.TypeLambda(
  params = List(TypeParam("X", 0)),
  body = TypeRepr.Applied(
    TypeRepr.ParamRef(paramF),
    List(TypeRepr.ParamRef(paramX))
  )
)

Wildcards and Bounds

// ?
TypeRepr.Wildcard()

// ? <: Upper
TypeRepr.Wildcard(TypeBounds.upper(upperType))

// ? >: Lower
TypeRepr.Wildcard(TypeBounds.lower(lowerType))

// ? >: Lower <: Upper
TypeRepr.Wildcard(TypeBounds(lowerType, upperType))

Parameter Modifiers

// => A (by-name)
TypeRepr.ByName(typeA)

// A* (varargs/repeated)
TypeRepr.Repeated(typeA)

// A @annotation
TypeRepr.Annotated(typeA, List(annotation))

Namespaces and Type Names

Owner

Owner represents where a type is defined in the package hierarchy:

// From package path
val owner = Owner.fromPackagePath("com.example.app")
// Owner(List(Package("com"), Package("example"), Package("app")))

// Build incrementally
val owner = Owner.Root / "com" / "example"

// Add term (object) segment
val owner = (Owner.Root / "com").term("MyObject")

// Add type segment
val owner = (Owner.Root / "com").tpe("MyClass")

Owner properties:

owner.asString    // "com.example" - dot-separated path
owner.isRoot      // true if empty
owner.parent      // Parent owner (or Root)
owner.lastName    // Last segment name

Predefined Owners

TypeId provides common namespaces:

Owner.scala                      // scala
Owner.scalaUtil                  // scala.util
Owner.scalaCollectionImmutable   // scala.collection.immutable
Owner.javaLang                   // java.lang
Owner.javaTime                   // java.time
Owner.javaUtil                   // java.util

TermPath

TermPath represents paths to term values (for singleton types):

// com.example.MyObject.value.type
val path = TermPath.fromOwner(
  Owner.fromPackagePath("com.example").term("MyObject"),
  "value"
)

path.asString     // "com.example.MyObject.value"
path.isEmpty      // false
path / "nested"   // Append segment

Type Parameters

TypeParam

Represents a type parameter specification:

// Basic type parameter
TypeParam("A", index = 0)

// Covariant (+A)
TypeParam("A", 0, Variance.Covariant)
TypeParam.covariant("A", 0)

// Contravariant (-A)
TypeParam("A", 0, Variance.Contravariant)
TypeParam.contravariant("A", 0)

// With bounds (A <: Upper)
TypeParam.bounded("A", 0, upper = TypeRepr.Ref(upperType))

// Higher-kinded (F[_])
TypeParam.higherKinded("F", 0, arity = 1)
TypeParam("F", 0, kind = Kind.Star1)

// Full specification
TypeParam(
  name = "A",
  index = 0,
  variance = Variance.Covariant,
  bounds = TypeBounds.upper(someType),
  kind = Kind.Type
)

TypeParam properties:

param.name              // "A"
param.index             // Position in parameter list
param.variance          // Covariant, Contravariant, or Invariant
param.bounds            // TypeBounds
param.kind              // Kind (*, * -> *, etc.)

param.isCovariant       // variance == Covariant
param.isContravariant   // variance == Contravariant
param.isInvariant       // variance == Invariant
param.hasUpperBound     // bounds.upper.isDefined
param.hasLowerBound     // bounds.lower.isDefined
param.isProperType      // kind == Kind.Type
param.isTypeConstructor // kind != Kind.Type

TypeBounds

Represents type parameter bounds:

// No bounds (>: Nothing <: Any)
TypeBounds.Unbounded

// Upper bound only (<: Upper)
TypeBounds.upper(upperType)

// Lower bound only (>: Lower)
TypeBounds.lower(lowerType)

// Both bounds (>: Lower <: Upper)
TypeBounds(lowerType, upperType)

// Type alias bounds (lower == upper)
TypeBounds.alias(aliasType)

TypeBounds properties:

bounds.lower            // Option[TypeRepr]
bounds.upper            // Option[TypeRepr]
bounds.isUnbounded      // No bounds specified
bounds.hasOnlyUpper     // Only upper bound
bounds.hasOnlyLower     // Only lower bound
bounds.hasBothBounds    // Both bounds specified
bounds.isAlias          // lower == upper
bounds.aliasType        // Option[TypeRepr] if alias

Variance

Variance.Covariant      // +A
Variance.Contravariant  // -A
Variance.Invariant      // A

variance.symbol         // "+", "-", or ""
variance.isCovariant
variance.isContravariant
variance.isInvariant
variance.flip           // Covariant <-> Contravariant
variance * other        // Combine variances

Kind

Represents the "kind" of a type (type of types):

Kind.Type               // * (proper type like Int, String)
Kind.Star               // Alias for Type
Kind.Star1              // * -> * (List, Option)
Kind.Star2              // * -> * -> * (Map, Either)
Kind.HigherStar1        // (* -> *) -> * (Functor, Monad)

Kind.constructor(0)     // *
Kind.constructor(1)     // * -> *
Kind.constructor(2)     // * -> * -> *

// Custom kinds
Kind.Arrow(List(Kind.Type), Kind.Type)  // * -> *
Kind.Arrow(List(Kind.Star1), Kind.Type) // (* -> *) -> *

Kind properties:

kind.isProperType   // kind == Kind.Type
kind.arity          // Number of type parameters

Members (Structural Types)

Val/Var Members

// val x: Int
Member.Val("x", TypeRepr.Ref(TypeId.int))

// var y: String
Member.Val("y", TypeRepr.Ref(TypeId.string), isVar = true)

Method Members

// def foo: Int
Member.Def("foo", Nil, Nil, TypeRepr.Ref(TypeId.int))

// def bar(x: Int): String
Member.Def(
  name = "bar",
  typeParams = Nil,
  paramLists = List(List(Param("x", TypeRepr.Ref(TypeId.int)))),
  result = TypeRepr.Ref(TypeId.string)
)

// def baz[A](x: A)(implicit y: Ordering[A]): List[A]
Member.Def(
  name = "baz",
  typeParams = List(TypeParam.A),
  paramLists = List(
    List(Param("x", TypeRepr.ParamRef(TypeParam.A))),
    List(Param("y", orderingA, isImplicit = true))
  ),
  result = listA
)

Type Members

// type T
Member.TypeMember("T")

// type T <: Upper
Member.TypeMember("T", upperBound = Some(upperType))

// type T = Alias (isAlias when lower == upper)
Member.TypeMember("T", 
  lowerBound = Some(aliasType),
  upperBound = Some(aliasType)
)

TypeDefKind

Classifies what kind of type definition a TypeId represents:

Class

TypeDefKind.Class(
  isFinal = false,
  isAbstract = false,
  isCase = true,       // case class
  isValue = false,     // extends AnyVal
  bases = List(...)    // parent types
)

Trait

TypeDefKind.Trait(
  isSealed = true,
  bases = List(...)
)

Object

TypeDefKind.Object(
  bases = List(...)
)

Enum (Scala 3)

TypeDefKind.Enum(
  bases = List(...)
)

TypeDefKind.EnumCase(
  parentEnum = parentEnumRef,
  ordinal = 0,
  isObjectCase = true
)

Type Aliases and Opaque Types

TypeDefKind.TypeAlias              // type Foo = Bar

TypeDefKind.OpaqueType(
  publicBounds = TypeBounds.Unbounded  // Bounds visible outside
)

TypeDefKind.AbstractType           // Abstract type member

Annotations

Represent Scala/Java annotations attached to types:

Annotation(
  typeId = TypeId.of[deprecated],
  args = List(
    AnnotationArg.Named("message", 
      AnnotationArg.Const("use newMethod")),
    AnnotationArg.Named("since", 
      AnnotationArg.Const("1.0"))
  )
)

Annotation argument types:

AnnotationArg.Const(value)           // Constant value
AnnotationArg.ArrayArg(values)       // Array of args
AnnotationArg.Named(name, value)     // Named parameter
AnnotationArg.Nested(annotation)     // Nested annotation
AnnotationArg.ClassOf(typeRepr)      // classOf[T]
AnnotationArg.EnumValue(enumType, valueName)  // Enum constant

Predefined TypeIds

TypeId provides instances for common types:

Primitives

TypeId.unit      // scala.Unit
TypeId.boolean   // scala.Boolean
TypeId.byte      // scala.Byte
TypeId.short     // scala.Short
TypeId.int       // scala.Int
TypeId.long      // scala.Long
TypeId.float     // scala.Float
TypeId.double    // scala.Double
TypeId.char      // scala.Char
TypeId.string    // java.lang.String
TypeId.bigInt    // scala.BigInt
TypeId.bigDecimal // scala.BigDecimal

Collections

TypeId.option     // scala.Option
TypeId.some       // scala.Some
TypeId.none       // scala.None
TypeId.list       // scala.collection.immutable.List
TypeId.vector     // scala.collection.immutable.Vector
TypeId.set        // scala.collection.immutable.Set
TypeId.seq        // scala.collection.immutable.Seq
TypeId.indexedSeq // scala.collection.immutable.IndexedSeq
TypeId.map        // scala.collection.immutable.Map
TypeId.either     // scala.util.Either
TypeId.array      // scala.Array
TypeId.arraySeq   // scala.collection.immutable.ArraySeq
TypeId.chunk      // zio.blocks.chunk.Chunk

java.time Types

TypeId.dayOfWeek      // java.time.DayOfWeek
TypeId.duration       // java.time.Duration
TypeId.instant        // java.time.Instant
TypeId.localDate      // java.time.LocalDate
TypeId.localDateTime  // java.time.LocalDateTime
TypeId.localTime      // java.time.LocalTime
TypeId.month          // java.time.Month
TypeId.monthDay       // java.time.MonthDay
TypeId.offsetDateTime // java.time.OffsetDateTime
TypeId.offsetTime     // java.time.OffsetTime
TypeId.period         // java.time.Period
TypeId.year           // java.time.Year
TypeId.yearMonth      // java.time.YearMonth
TypeId.zoneId         // java.time.ZoneId
TypeId.zoneOffset     // java.time.ZoneOffset
TypeId.zonedDateTime  // java.time.ZonedDateTime

java.util Types

TypeId.currency   // java.util.Currency
TypeId.uuid       // java.util.UUID

Integration with Schema

TypeId is central to ZIO Blocks' schema system. Every Reflect node has an associated TypeId:

import zio.blocks.schema._

case class Person(name: String, age: Int)
object Person {
  implicit val schema: Schema[Person] = Schema.derived
}

// Access TypeId from schema
val reflect = Schema[Person].reflect
val typeId = reflect.typeId

typeId.name        // "Person"
typeId.isCaseClass // true

Schema Transformations

TypeId is captured when transforming schemas:

case class Email(value: String)

object Email {
  implicit val schema: Schema[Email] = Schema[String]
    .transform(Email(_), _.value)
    .withTypeName[Email]  // Sets TypeId to Email
}

Schema Derivation

The Deriver trait receives TypeId for each node:

trait Deriver[TC[_]] {
  def deriveRecord[A](
    typeId: TypeId[A],
    fields: => Chunk[Deriver.Field[TC, A, _]],
    ...
  ): TC[A]
  
  def deriveVariant[A](
    typeId: TypeId[A],
    cases: => Chunk[Deriver.Case[TC, A, _]],
    ...
  ): TC[A]
  
  // ... other methods
}

Type Normalization

Type aliases are normalized to their underlying types for comparison:

type Age = Int

val ageId = TypeId.alias[Age]("Age", owner, Nil, TypeRepr.Ref(TypeId.int))
val normalized = TypeId.normalize(ageId)

normalized.fullName  // "scala.Int" (not "Age")

Normalization handles nested aliases and type arguments:

type IntList = List[Int]
type MyIntList = IntList

// Normalizing MyIntList resolves through IntList to List[Int]

Equality and Hashing

TypeId uses structural equality that accounts for type aliases:

val alias1 = TypeId.alias[A]("A", owner, Nil, TypeRepr.Ref(TypeId.int))
val alias2 = TypeId.alias[A]("A", owner, Nil, TypeRepr.Ref(TypeId.int))

alias1 == alias2  // true (structural equality)

// Works correctly in hash maps
val map = Map(alias1 -> "value")
map(alias2)  // "value"

Erased TypeId

For type-indexed collections where the type parameter doesn't matter:

// TypeId.Erased is TypeId[TypeId.Unknown]
val erased: TypeId.Erased = typeId.erased

// Use in maps keyed by type
val typeRegistry: Map[TypeId.Erased, Schema[_]] = Map(
  TypeId.int.erased -> Schema[Int],
  TypeId.string.erased -> Schema[String]
)

Runtime Reflection

On JVM, TypeId can retrieve the corresponding Class:

val typeId = TypeId.of[Person]
val clazz: Option[Class[_]] = typeId.clazz

// Construct instances (JVM only)
val result: Either[String, Any] = typeId.construct(Chunk("Alice", 30))