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))