@zio.dev/zio-blocks 0.0.32 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +293 -51
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +3 -3
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +34 -192
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +5 -5
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +3 -3
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +13 -1
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +2922 -583
  138. package/sidebars.js +238 -43
  139. package/superpowers/plans/2026-03-19-docs-critique-subagent.md +407 -0
  140. package/superpowers/specs/2026-03-19-docs-critique-subagent-design.md +222 -0
  141. package/reference/formats.md +0 -694
  142. package/reference/http-model.md +0 -1716
  143. package/reference/streams.md +0 -989
  144. package/ringbuffer.md +0 -249
  145. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  146. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  147. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  148. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  149. /package/reference/{registers.md → schema/registers.md} +0 -0
  150. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  151. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  152. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -3,898 +3,3237 @@ id: typeid
3
3
  title: "TypeId"
4
4
  ---
5
5
 
6
- # TypeId
6
+ `TypeId[A]` represents the identity of a type or type constructor at runtime — it captures complete type metadata (names, type parameters, parent types, annotations, classification) that would otherwise be erased by the JVM and Scala.js. Use `TypeId` when you need to preserve full type information as data for serialization, code generation, registry lookups, or type-safe dispatching.
7
7
 
8
- `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.
8
+ In Scala and the JVM, compile-time type information is erased at runtime. This means generic type parameters, sealed trait variants, and even opaque types become indistinguishable at runtime — `List[Int]` and `List[String]` both look like `List` to the JVM. This erasure makes it nearly impossible to implement universal serializers that work across formats (JSON, YAML, XML, MessagePack), code generators, or schema-driven transformations without losing semantic information. `TypeId` solves this by capturing complete type structure at compile time and making it available as a hashable, inspectable value at runtime.
9
9
 
10
- ## Overview
10
+ The `TypeId` trait exposes the type's structure through a rich set of properties and predicates:
11
11
 
12
- TypeId is fundamental to ZIO Blocks' schema system, enabling:
12
+ ```scala
13
+ // Simplified — some members shown here are derived from abstract members
14
+ sealed trait TypeId[A <: AnyKind] {
15
+ // Abstract members
16
+ def name: String
17
+ def owner: Owner
18
+ def typeParams: List[TypeParam]
19
+ def typeArgs: List[TypeRepr]
20
+ def defKind: TypeDefKind
21
+ def selfType: Option[TypeRepr] // Self-type annotation, if any
22
+ def aliasedTo: Option[TypeRepr] // Target type for type aliases
23
+ def representation: Option[TypeRepr] // Underlying type for opaque types
24
+ def annotations: List[Annotation]
25
+
26
+ // Derived properties
27
+ final def fullName: String // owner.asString + "." + name
28
+ final def arity: Int // typeParams.size
29
+ final def isCaseClass: Boolean
30
+ final def isSealed: Boolean
31
+ final def isAlias: Boolean
32
+ // ... many more derived predicates
33
+ }
34
+ ```
35
+
36
+ > In Scala 3, `A` is bounded by `AnyKind` to support higher-kinded types. In Scala 2, the bound is omitted (`sealed trait TypeId[A]`).
13
37
 
14
- - **Type identification** - Uniquely identify types across serialization boundaries
15
- - **Subtype checking** - Determine inheritance relationships at runtime
16
- - **Type normalization** - Resolve type aliases to their underlying types
17
- - **Schema derivation** - Automatically derive schemas for user-defined types
38
+ Derive a `TypeId` for any type using the `TypeId.of` macro and then inspect the type's structure at runtime:
18
39
 
19
40
  ```scala
20
41
  import zio.blocks.typeid._
21
42
 
22
- // Derive TypeId for your types
23
43
  case class Person(name: String, age: Int)
24
- val personId: TypeId[Person] = TypeId.of[Person]
25
44
 
26
- // Access type information
27
- personId.name // "Person"
28
- personId.fullName // "com.example.Person"
29
- personId.isCaseClass // true
45
+ val id = TypeId.of[Person]
46
+ ```
30
47
 
31
- // Use predefined TypeIds
32
- TypeId.int.fullName // "scala.Int"
33
- TypeId.string.fullName // "java.lang.String"
34
- TypeId.list.arity // 1 (type constructor)
48
+ ```scala
49
+ id.name
50
+ // res1: String = "Person"
51
+ id.fullName
52
+ // res2: String = "repl.MdocSession.MdocApp0.Person"
53
+ id.isCaseClass
54
+ // res3: Boolean = true
35
55
  ```
36
56
 
57
+ ## Motivation
58
+
59
+ Standard approaches to preserving type information at runtime — `ClassTag`, `TypeTag` (Scala 2), `TypeTest` (Scala 3) — each have limitations. `ClassTag` loses generic type arguments. `TypeTag` depends on `scala-reflect` and is unavailable on Scala.js. `TypeTest` only answers "is this value an instance of T?" without exposing type structure. None of them distinguish opaque types from their underlying representation.
60
+
61
+ TypeId takes a different approach: the `TypeId.of` macro captures type metadata at compile time and stores it as a plain, immutable data structure — no runtime reflection, no platform-specific APIs. This makes it suitable as a foundation for cross-platform schema systems, code generators, and type-indexed registries.
62
+
37
63
  ## Installation
38
64
 
39
65
  TypeId is included in the `zio-blocks-typeid` module. Add it to your build:
40
66
 
41
67
  ```scala
42
- libraryDependencies += "dev.zio" %% "zio-blocks-typeid" % "<version>"
68
+ libraryDependencies += "dev.zio" %% "zio-blocks-typeid" % "@VERSION"
43
69
  ```
44
70
 
45
- Cross-platform support: TypeId works on JVM and Scala.js.
71
+ For cross-platform (Scala.js):
46
72
 
47
- ## Creating TypeIds
48
-
49
- ### Automatic Derivation
73
+ ```scala
74
+ libraryDependencies += "dev.zio" %%% "zio-blocks-typeid" % "@VERSION"
75
+ ```
50
76
 
51
- The simplest way to get a TypeId is via macro derivation:
77
+ Supported Scala versions: 2.13.x and 3.x.
52
78
 
53
- ```scala
54
- import zio.blocks.typeid._
79
+ ## Creating Instances
55
80
 
56
- case class User(id: Long, email: String)
81
+ There are two approaches to creating `TypeId` values: **automatic derivation** (recommended for normal use) and **manual construction** (for advanced metaprogramming scenarios).
57
82
 
58
- // Scala 3
59
- val userId: TypeId[User] = TypeId.of[User]
83
+ ### Automatic Derivation
60
84
 
61
- // Scala 2
62
- val userId: TypeId[User] = TypeId.of[User]
85
+ For most users and most types, automatic derivation via `TypeId.of` or implicit `derived` is the right choice. These macros extract complete type metadata at compile time, handling all type variants correctly.
63
86
 
64
- // Or use implicit derivation
65
- val userId: TypeId[User] = implicitly[TypeId[User]]
66
- ```
87
+ #### `TypeId.of` — Macro Derivation
67
88
 
68
- The macro extracts complete type information including:
69
- - Type name and owner
70
- - Type parameters and variance
71
- - Parent types (for sealed traits and enums)
72
- - Whether it's a case class, sealed trait, enum, etc.
89
+ The primary way to obtain a `TypeId` is through the `TypeId.of[A]` macro, which extracts complete type metadata at compile time:
73
90
 
74
- ### Manual Construction
91
+ ```scala
92
+ object TypeId {
93
+ inline def of[A <: AnyKind]: TypeId[A] // Scala 3
94
+ def of[A]: TypeId[A] // Scala 2 (macro)
95
+ }
96
+ ```
75
97
 
76
- For manual type registration or testing, use smart constructors:
98
+ Derive a TypeId using the macro:
77
99
 
78
100
  ```scala
79
- // Nominal types (classes, traits, objects)
80
- val myTypeId = TypeId.nominal[MyType](
81
- name = "MyType",
82
- owner = Owner.fromPackagePath("com.example"),
83
- defKind = TypeDefKind.Class(isCase = true)
84
- )
101
+ import zio.blocks.typeid._
85
102
 
86
- // Type aliases
87
- val aliasId = TypeId.alias[Age](
88
- name = "Age",
89
- owner = Owner.fromPackagePath("com.example"),
90
- aliased = TypeRepr.Ref(TypeId.int)
91
- )
103
+ case class User(id: Long, email: String)
104
+ ```
92
105
 
93
- // Opaque types (Scala 3)
94
- val emailId = TypeId.opaque[Email](
95
- name = "Email",
96
- owner = Owner.fromPackagePath("com.example"),
97
- representation = TypeRepr.Ref(TypeId.string)
98
- )
106
+ ```scala
107
+ val userId = TypeId.of[User]
108
+ // userId: TypeId[User] = User
109
+ userId.name
110
+ // res5: String = "User"
111
+ userId.fullName
112
+ // res6: String = "repl.MdocSession.MdocApp4.User"
113
+ userId.isCaseClass
114
+ // res7: Boolean = true
99
115
  ```
100
116
 
101
- ### Applied Types
117
+ #### `TypeId.derived` — Implicit Derivation
102
118
 
103
- Create applied types (type constructors with arguments):
119
+ TypeId instances are available implicitly through the `derived` macro. Any function that requires a `TypeId[A]` in implicit scope will have it derived automatically — you never need to pass it manually.
104
120
 
105
- ```scala
106
- // List[Int]
107
- val listIntId = TypeId.applied[List[Int]](
108
- TypeId.list,
109
- TypeRepr.Ref(TypeId.int)
110
- )
121
+ From the user's perspective, the API is:
111
122
 
112
- // Map[String, Int]
113
- val mapId = TypeId.applied[Map[String, Int]](
114
- TypeId.map,
115
- TypeRepr.Ref(TypeId.string),
116
- TypeRepr.Ref(TypeId.int)
117
- )
123
+ ```scala
124
+ object TypeId {
125
+ inline given derived[A <: AnyKind]: TypeId[A] // Scala 3
126
+ implicit def derived[A]: TypeId[A] // Scala 2 (macro)
127
+ }
118
128
  ```
119
129
 
120
- ## TypeId Properties
130
+ :::note
131
+ In Scala 3, the `[A <: AnyKind]` bound allows derivation for type constructors (e.g., `TypeId[List]`). In Scala 2, the bound is `[A]` and type constructor derivation uses `TypeId[List[_]]` syntax instead.
132
+ :::
121
133
 
122
- ### Basic Properties
134
+ The most common use case is accepting `TypeId[A]` as an implicit parameter:
123
135
 
124
136
  ```scala
125
- val id = TypeId.of[Person]
137
+ import zio.blocks.typeid._
126
138
 
127
- id.name // "Person" - simple name
128
- id.fullName // "com.example.Person" - fully qualified
129
- id.owner // Owner representing the package/enclosing type
130
- id.arity // 0 for proper types, n for type constructors
131
- id.typeParams // List of TypeParam for type constructors
132
- id.typeArgs // List of TypeRepr for applied types
139
+ case class User(id: Long, email: String)
140
+
141
+ def describe[A](implicit typeId: TypeId[A]): String =
142
+ s"${typeId.fullName} is a case class: ${typeId.isCaseClass}"
133
143
  ```
134
144
 
135
- ### Type Classification
145
+ Call the function with the type argument — the TypeId is derived automatically:
136
146
 
137
147
  ```scala
138
- id.isClass // true for classes
139
- id.isTrait // true for traits
140
- id.isObject // true for singleton objects
141
- id.isEnum // true for Scala 3 enums
142
- id.isCaseClass // true for case classes
143
- id.isValueClass // true for value classes (extends AnyVal)
144
- id.isSealed // true for sealed traits
145
- id.isAlias // true for type aliases
146
- id.isOpaque // true for opaque types
147
- id.isAbstract // true for abstract type members
148
-
149
- id.isProperType // arity == 0
150
- id.isTypeConstructor // arity > 0
151
- id.isApplied // has type arguments
148
+ describe[User]
149
+ // res9: String = "repl.MdocSession.MdocApp8.User is a case class: true"
150
+ describe[Int]
151
+ // res10: String = "scala.Int is a case class: false"
152
152
  ```
153
153
 
154
- ### Common Type Checks
154
+ You can also summon a TypeId explicitly with `implicitly` (Scala 2) or `summon` (Scala 3):
155
155
 
156
156
  ```scala
157
- id.isTuple // scala.TupleN
158
- id.isProduct // scala.ProductN
159
- id.isSum // Either or Option
160
- id.isEither // scala.util.Either
161
- id.isOption // scala.Option
157
+ val userTypeId = implicitly[TypeId[User]]
158
+ // userTypeId: TypeId[User] = User
159
+ userTypeId.name
160
+ // res11: String = "User"
162
161
  ```
163
162
 
164
- ### Subtype Relationships
163
+ When you need the TypeId in a single expression, use `TypeId.of[A]`. For generic functions that accept any `A` and need its TypeId alongside other implicit evidence, use implicit derivation instead.
165
164
 
166
- ```scala
167
- sealed trait Animal
168
- case class Dog(name: String) extends Animal
165
+ ### Manual Derivation (Smart Constructors)
169
166
 
170
- val dogId = TypeId.of[Dog]
171
- val animalId = TypeId.of[Animal]
167
+ For advanced use cases — unit testing with synthetic metadata, code generators that create types dynamically, or frameworks that construct TypeIds at runtime — the smart constructor functions allow you to manually assemble TypeIds by specifying their components. These are never needed in normal user code, since `TypeId.of` handles all these cases automatically.
172
168
 
173
- dogId.isSubtypeOf(animalId) // true
174
- animalId.isSupertypeOf(dogId) // true
175
- dogId.isEquivalentTo(dogId) // true
176
- ```
169
+ #### `TypeId.nominal` — Nominal Types
177
170
 
178
- Subtype checking handles:
179
- - Direct inheritance
180
- - Enum cases and their parent enums
181
- - Sealed trait subtypes
182
- - Transitive inheritance
183
- - Variance-aware subtyping for applied types
171
+ **Nominal types** are concrete type definitions: classes, traits, and objects. In contrast to [type aliases](#typeidalias--type-aliases) (which are alternative names for existing types) and [opaque types](#typeidopaque--opaque-types) (which have a hidden representation), nominal types stand as distinct, named types in the type system.
184
172
 
185
- ### Pattern Matching
173
+ For most end users, you don't need to use `TypeId.nominal` directly. The `TypeId.of` macro automatically derives nominal TypeIds from your actual type definitions at compile time. The `nominal` smart constructor exists for **advanced use cases**: unit testing with synthetic type metadata, code generators that create types dynamically at runtime, or frameworks that assemble TypeIds programmatically. Unless you're in one of these scenarios, `TypeId.of` is the right tool.
186
174
 
187
- TypeId provides extractors for pattern matching:
175
+ If you do need to construct nominal TypeIds manually, the API provides two overloads:
188
176
 
189
177
  ```scala
190
- typeId match {
191
- case TypeId.Nominal(name, owner, params, defKind, parents) =>
192
- // Regular types
193
-
194
- case TypeId.Alias(name, owner, params, aliased) =>
195
- // Type aliases - aliased is the underlying TypeRepr
196
-
197
- case TypeId.Opaque(name, owner, params, repr, bounds) =>
198
- // Opaque types - repr is the representation type
199
-
200
- case TypeId.Sealed(name) =>
201
- // Sealed traits
202
-
203
- case TypeId.Enum(name, owner) =>
204
- // Scala 3 enums
178
+ object TypeId {
179
+ def nominal[A <: AnyKind](name: String, owner: Owner, kind: TypeDefKind): TypeId[A]
180
+
181
+ def nominal[A <: AnyKind](
182
+ name: String, owner: Owner,
183
+ typeParams: List[TypeParam] = Nil, typeArgs: List[TypeRepr] = Nil,
184
+ defKind: TypeDefKind = TypeDefKind.Unknown,
185
+ selfType: Option[TypeRepr] = None,
186
+ annotations: List[Annotation] = Nil
187
+ ): TypeId[A]
205
188
  }
206
189
  ```
207
190
 
208
- ## TypeRepr
191
+ #### `TypeId.alias` — Type Aliases
209
192
 
210
- `TypeRepr` represents type expressions in the Scala type system. While `TypeId` identifies a type definition, `TypeRepr` represents how types are used in expressions.
193
+ **Type aliases** are alternative names for existing types. For example, `type Age = Int` creates an alias for `Int` so code can read `Age` instead of `Int`. TypeIds for type aliases preserve the distinction from their underlying type through the `aliasedTo` property, enabling alias-aware serialization and schema generation.
211
194
 
212
- ### Basic Type References
195
+ For normal use, you don't need `TypeId.alias` directly. When you write a type alias in your code (e.g., `type UserId = String`), the `TypeId.of` macro automatically derives the correct TypeId. The `alias` smart constructor is for **advanced use cases**: unit testing with synthetic alias metadata, code generators that create type aliases dynamically at runtime, or frameworks that normalize or transform type aliases during schema processing. Unless you're building one of these, `TypeId.of` is the right tool.
213
196
 
214
- ```scala
215
- // Reference to a named type
216
- TypeRepr.Ref(TypeId.int) // Int
217
- TypeRepr.Ref(TypeId.string) // String
197
+ For testing or code generation, construct an alias TypeId:
218
198
 
219
- // Reference to a type parameter
220
- TypeRepr.ParamRef(TypeParam.A) // A
221
- TypeRepr.ParamRef(param, depth = 1) // nested binder reference
199
+ ```scala
200
+ object TypeId {
201
+ def alias[A <: AnyKind](
202
+ name: String, owner: Owner,
203
+ typeParams: List[TypeParam] = Nil,
204
+ aliased: TypeRepr,
205
+ typeArgs: List[TypeRepr] = Nil,
206
+ annotations: List[Annotation] = Nil
207
+ ): TypeId[A]
208
+ }
222
209
  ```
223
210
 
224
- ### Applied Types
225
-
226
211
  ```scala
227
- // List[Int]
228
- TypeRepr.Applied(
229
- TypeRepr.Ref(TypeId.list),
230
- List(TypeRepr.Ref(TypeId.int))
231
- )
212
+ import zio.blocks.typeid._
213
+ ```
232
214
 
233
- // Map[String, Int]
234
- TypeRepr.Applied(
235
- TypeRepr.Ref(TypeId.map),
236
- List(TypeRepr.Ref(TypeId.string), TypeRepr.Ref(TypeId.int))
237
- )
215
+ ```scala
216
+ val ageId = TypeId.alias[Any]("Age", Owner.fromPackagePath("com.example"), aliased = TypeRepr.Ref(TypeId.int))
217
+ // ageId: TypeId[Any] = Age
218
+ ageId.isAlias
219
+ // res13: Boolean = true
220
+ ageId.aliasedTo
221
+ // res14: Option[TypeRepr] = Some(Ref(Int))
238
222
  ```
239
223
 
240
- ### Compound Types
224
+ #### `TypeId.opaque` — Opaque Types
241
225
 
242
- ```scala
243
- // Intersection: A & B (Scala 3) or A with B (Scala 2)
244
- TypeRepr.Intersection(List(typeA, typeB))
226
+ **Opaque types** (a Scala 3 feature) are types that have a distinct compile-time identity but a hidden runtime representation. For example, `opaque type UserId = String` creates a type that is distinct from `String` at compile time, but represents `String` at runtime. TypeId preserves this distinction, unlike standard reflection which erases opaque types to their underlying type — a critical capability for type-safe serialization and validation.
245
227
 
246
- // Union: A | B (Scala 3 only)
247
- TypeRepr.Union(List(typeA, typeB))
228
+ For normal use, you don't need `TypeId.opaque` directly. When you define an opaque type in your code, the `TypeId.of` macro automatically derives the correct TypeId with its representation. The `opaque` smart constructor is for **advanced use cases**: unit testing with synthetic opaque type metadata, code generators that create opaque types dynamically, or frameworks that need to construct type metadata for dynamically-discovered opaque types. Unless you're building one of these, `TypeId.of` is the right tool.
248
229
 
249
- // Convenience constructors handle edge cases
250
- TypeRepr.intersection(List(typeA)) // returns typeA (not Intersection)
251
- TypeRepr.intersection(Nil) // returns AnyType
252
- TypeRepr.union(List(typeA)) // returns typeA
253
- TypeRepr.union(Nil) // returns NothingType
230
+ For testing or code generation, construct an opaque TypeId:
231
+
232
+ ```scala
233
+ object TypeId {
234
+ def opaque[A <: AnyKind](
235
+ name: String, owner: Owner,
236
+ typeParams: List[TypeParam] = Nil,
237
+ representation: TypeRepr,
238
+ typeArgs: List[TypeRepr] = Nil,
239
+ publicBounds: TypeBounds = TypeBounds.Unbounded,
240
+ annotations: List[Annotation] = Nil
241
+ ): TypeId[A]
242
+ }
254
243
  ```
255
244
 
256
- ### Function Types
245
+ #### `TypeId.applied` — Applied Types
257
246
 
258
- ```scala
259
- // A => B
260
- TypeRepr.Function(List(typeA), typeB)
247
+ **Applied types** are generic types instantiated with type arguments. For example, `List[Int]` is `List` (the type constructor) applied to `Int` (the type argument), and `Map[String, Int]` is `Map` applied to two type arguments. TypeIds for applied types preserve the type arguments so serializers can generate specialized codecs, validators can type-check values, and code generators can emit correct code.
261
248
 
262
- // (A, B) => C
263
- TypeRepr.Function(List(typeA, typeB), typeC)
249
+ For normal use, you don't need `TypeId.applied` directly. When you write an applied type in your code (e.g., `List[Int]` or `Map[String, User]`), the `TypeId.of` macro automatically derives the correct TypeId with its type arguments preserved. The `applied` smart constructor is for **advanced use cases**: unit testing with synthetic applied type metadata, code generators that construct type expressions dynamically, or frameworks that need to build type metadata at runtime for dynamically-discovered generic types. Unless you're building one of these, `TypeId.of` is the right tool.
264
250
 
265
- // (A, B) ?=> C (context function, Scala 3)
266
- TypeRepr.ContextFunction(List(typeA, typeB), typeC)
251
+ For testing or code generation, construct applied TypeIds by combining a type constructor with type argument expressions:
252
+
253
+ ```scala
254
+ object TypeId {
255
+ def applied[A <: AnyKind](typeConstructor: TypeId[?], args: TypeRepr*): TypeId[A]
256
+ }
267
257
  ```
268
258
 
269
- ### Tuple Types
259
+ ## Core Operations
260
+
261
+ This section documents all public methods on `TypeId` and its companion object, organized by category.
262
+
263
+ ### Identity and Naming
264
+
265
+ These methods provide the type's name and fully qualified path.
266
+
267
+ #### `name` — Simple Type Name
268
+
269
+ Returns the unqualified name of the type:
270
+
271
+ ```scala
272
+ sealed trait TypeId[A <: AnyKind] {
273
+ def name: String
274
+ }
275
+ ```
270
276
 
271
277
  ```scala
272
- // (A, B, C)
273
- TypeRepr.Tuple(List(
274
- TupleElement(None, typeA),
275
- TupleElement(None, typeB),
276
- TupleElement(None, typeC)
277
- ))
278
+ import zio.blocks.typeid._
278
279
 
279
- // Named tuples (Scala 3.5+): (name: String, age: Int)
280
- TypeRepr.Tuple(List(
281
- TupleElement(Some("name"), TypeRepr.Ref(TypeId.string)),
282
- TupleElement(Some("age"), TypeRepr.Ref(TypeId.int))
283
- ))
280
+ case class Order(id: String, total: Double)
281
+ val orderId = TypeId.of[Order]
282
+ ```
284
283
 
285
- // Convenience for unnamed tuples
286
- TypeRepr.tuple(List(typeA, typeB, typeC))
284
+ ```scala
285
+ orderId.name
286
+ // res16: String = "Order"
287
+ TypeId.int.name
288
+ // res17: String = "Int"
289
+ TypeId.list.name
290
+ // res18: String = "List"
287
291
  ```
288
292
 
289
- ### Structural Types
293
+ #### `fullName` — Fully Qualified Name
294
+
295
+ Returns `owner.asString + "." + name`, or just `name` if the owner is root:
290
296
 
291
297
  ```scala
292
- // { def foo: Int }
293
- TypeRepr.Structural(
294
- parents = Nil,
295
- members = List(
296
- Member.Def("foo", Nil, Nil, TypeRepr.Ref(TypeId.int))
297
- )
298
- )
298
+ sealed trait TypeId[A <: AnyKind] {
299
+ def fullName: String
300
+ }
301
+ ```
299
302
 
300
- // AnyRef { type T; val x: T }
301
- TypeRepr.Structural(
302
- parents = List(TypeRepr.Ref(anyRefId)),
303
- members = List(
304
- Member.TypeMember("T"),
305
- Member.Val("x", TypeRepr.ParamRef(paramT))
306
- )
307
- )
303
+ ```scala
304
+ orderId.fullName
305
+ // res19: String = "repl.MdocSession.MdocApp15.Order"
306
+ TypeId.int.fullName
307
+ // res20: String = "scala.Int"
308
+ TypeId.string.fullName
309
+ // res21: String = "java.lang.String"
308
310
  ```
309
311
 
310
- ### Path-Dependent and Singleton Types
312
+ #### `owner` — Enclosing Namespace
313
+
314
+ The `TypeId#owner` method returns the `Owner` — the hierarchical path showing exactly where a type is defined. This includes the complete package chain and any enclosing objects or types. `Owner` solves a critical problem: multiple types can have the same name (e.g., `User` in `com.api` and `User` in `com.admin`), and the owner uniquely distinguishes them by their definition location:
311
315
 
312
316
  ```scala
313
- // x.type (singleton type)
314
- TypeRepr.Singleton(TermPath.fromOwner(owner, "x"))
317
+ sealed trait TypeId[A <: AnyKind] {
318
+ def owner: Owner
319
+ }
320
+ ```
315
321
 
316
- // this.type
317
- TypeRepr.ThisType(owner)
322
+ When we derive a `TypeId` for a custom type, the owner captures its full hierarchical location. We can then use `TypeId#fullName` to see how the owner combines with the type name:
323
+
324
+ ```scala
325
+ import zio.blocks.typeid._
318
326
 
319
- // Outer#Inner (type projection)
320
- TypeRepr.TypeProjection(outerType, "Inner")
327
+ case class User(id: Long, name: String)
328
+ ```
321
329
 
322
- // qualifier.Member (type selection)
323
- TypeRepr.TypeSelect(qualifierType, "Member")
330
+ ```scala
331
+ val userId = TypeId.of[User]
332
+ // userId: TypeId[User] = User
333
+ userId.name
334
+ // res23: String = "User"
335
+ // The owner shows where this User is defined
336
+ userId.owner.asString
337
+ // res24: String = "repl.MdocSession.MdocApp22"
338
+ // fullName combines owner and name into a qualified path
339
+ userId.fullName
340
+ // res25: String = "repl.MdocSession.MdocApp22.User"
324
341
  ```
325
342
 
326
- ### Special Types
343
+ When we construct types from different packages, their owners differ even though the names are identical. This is essential for registries and serializers that need to distinguish between types with conflicting names:
327
344
 
328
345
  ```scala
329
- TypeRepr.AnyType // Any
330
- TypeRepr.NothingType // Nothing
331
- TypeRepr.NullType // Null
332
- TypeRepr.UnitType // Unit
333
- TypeRepr.AnyKindType // AnyKind (for kind-polymorphic contexts)
346
+ // A User from the admin domain
347
+ val adminUser = TypeId.nominal[Any]("User", Owner.fromPackagePath("com.admin"), TypeDefKind.Unknown)
348
+ // adminUser: TypeId[Any] = User
349
+ adminUser.name
350
+ // res26: String = "User"
351
+ // Notice the owner is different
352
+ adminUser.owner.asString
353
+ // res27: String = "com.admin"
354
+ // So the full names are distinct
355
+ adminUser.fullName
356
+ // res28: String = "com.admin.User"
357
+
358
+ // Compare: both have name "User" but different owners
359
+ userId.name == adminUser.name
360
+ // res29: Boolean = true
361
+ userId.owner.asString == adminUser.owner.asString
362
+ // res30: Boolean = false
334
363
  ```
335
364
 
336
- ### Constant/Literal Types
365
+ This distinction enables type-indexed registries where you can safely store types with identical names from different sources without collision.
366
+
367
+ #### `toString` — Idiomatic Scala Rendering
368
+
369
+ Renders the TypeId as idiomatic Scala syntax using `TypeIdPrinter`:
337
370
 
338
371
  ```scala
339
- TypeRepr.Constant.IntConst(42) // 42 (literal type)
340
- TypeRepr.Constant.StringConst("foo") // "foo"
341
- TypeRepr.Constant.BooleanConst(true) // true
342
- TypeRepr.Constant.ClassOfConst(tpe) // classOf[T]
372
+ TypeId.of[List[Int]].toString
373
+ // res31: String = "List[Int]"
374
+ TypeId.of[Map[String, Int]].toString
375
+ // res32: String = "Map[String, Int]"
343
376
  ```
344
377
 
345
- ### Type Lambdas (Scala 3)
378
+ ### Type Parameters and Arguments
379
+
380
+ Methods for inspecting generic type information.
381
+
382
+ #### `typeParams` — Formal Type Parameters
383
+
384
+ The `TypeId#typeParams` method returns the list of formal type parameters declared by a type. This is what makes a type generic. For a type like `Box[+A]`, `typeParams` captures the declaration of `A` — including its name, position in the parameter list, variance (whether it's covariant `+`, contravariant `−`, or invariant), and any bounds:
346
385
 
347
386
  ```scala
348
- // [X] =>> F[X]
349
- TypeRepr.TypeLambda(
350
- params = List(TypeParam("X", 0)),
351
- body = TypeRepr.Applied(
352
- TypeRepr.ParamRef(paramF),
353
- List(TypeRepr.ParamRef(paramX))
354
- )
355
- )
387
+ sealed trait TypeId[A <: AnyKind] {
388
+ def typeParams: List[TypeParam]
389
+ }
356
390
  ```
357
391
 
358
- ### Wildcards and Bounds
392
+ To see how `TypeId` preserves type parameter information, we define several generic types with different variance patterns. Each demonstrates a different type parameter characteristic:
359
393
 
360
394
  ```scala
361
- // ?
362
- TypeRepr.Wildcard()
395
+ import zio.blocks.typeid._
363
396
 
364
- // ? <: Upper
365
- TypeRepr.Wildcard(TypeBounds.upper(upperType))
397
+ sealed trait Container[+A]
398
+ case class Box[+A](value: A) extends Container[A]
366
399
 
367
- // ? >: Lower
368
- TypeRepr.Wildcard(TypeBounds.lower(lowerType))
400
+ sealed trait Sink[-T]
401
+ case class Logger[-T]() extends Sink[T]
369
402
 
370
- // ? >: Lower <: Upper
371
- TypeRepr.Wildcard(TypeBounds(lowerType, upperType))
403
+ sealed trait Cache[K, +V]
404
+ case class LRUCache[K, +V](maxSize: Int) extends Cache[K, V]
372
405
  ```
373
406
 
374
- ### Parameter Modifiers
407
+ When we derive `TypeId` for these types, we can inspect their type parameters and see the variance that was declared:
375
408
 
376
409
  ```scala
377
- // => A (by-name)
378
- TypeRepr.ByName(typeA)
410
+ val boxId = TypeId.of[Box]
411
+ // boxId: TypeId[[A >: Nothing <: Any] =>> Box[A]] = Box[+A]
412
+ // Box declares [+A], so we see one covariant parameter
413
+ boxId.typeParams
414
+ // res34: List[TypeParam] = List(
415
+ // TypeParam(
416
+ // name = "A",
417
+ // index = 0,
418
+ // variance = Covariant,
419
+ // bounds = TypeBounds(lower = None, upper = None),
420
+ // kind = Type
421
+ // )
422
+ // )
423
+ boxId.typeParams.head.variance
424
+ // res35: Variance = Covariant
425
+ boxId.typeParams.head.name
426
+ // res36: String = "A"
427
+
428
+ val sinkId = TypeId.of[Sink]
429
+ // sinkId: TypeId[[T >: Nothing <: Any] =>> Sink[T]] = Sink[-T]
430
+ // Sink declares [-T], so we see one contravariant parameter
431
+ sinkId.typeParams
432
+ // res37: List[TypeParam] = List(
433
+ // TypeParam(
434
+ // name = "T",
435
+ // index = 0,
436
+ // variance = Contravariant,
437
+ // bounds = TypeBounds(lower = None, upper = None),
438
+ // kind = Type
439
+ // )
440
+ // )
441
+ sinkId.typeParams.head.variance
442
+ // res38: Variance = Contravariant
443
+
444
+ val cacheId = TypeId.of[Cache]
445
+ // cacheId: TypeId[[K >: Nothing <: Any, V >: Nothing <: Any] =>> Cache[K, V]] = Cache[K, +V]
446
+ // Cache declares [K, +V], so we see two parameters with different variances
447
+ cacheId.typeParams
448
+ // res39: List[TypeParam] = List(
449
+ // TypeParam(
450
+ // name = "K",
451
+ // index = 0,
452
+ // variance = Invariant,
453
+ // bounds = TypeBounds(lower = None, upper = None),
454
+ // kind = Type
455
+ // ),
456
+ // TypeParam(
457
+ // name = "V",
458
+ // index = 1,
459
+ // variance = Covariant,
460
+ // bounds = TypeBounds(lower = None, upper = None),
461
+ // kind = Type
462
+ // )
463
+ // )
464
+ cacheId.typeParams.map(p => (p.name, p.variance.symbol))
465
+ // res40: List[Tuple2[String, String]] = List(("K", ""), ("V", "+"))
466
+ ```
467
+
468
+ #### `TypeParam` — Type Parameter Details
379
469
 
380
- // A* (varargs/repeated)
381
- TypeRepr.Repeated(typeA)
470
+ Each element in `TypeId#typeParams` is a `TypeParam` value. A type parameter defines a single formal parameter in a generic type's declaration — its name, position, variance (covariance, contravariance, invariance), bounds, and kind. When you derive a `TypeId` for a generic type like `Box[+A]`, the macro captures each declared parameter as a `TypeParam` so you can inspect them at runtime.
382
471
 
383
- // A @annotation
384
- TypeRepr.Annotated(typeA, List(annotation))
472
+ `TypeParam` captures these pieces of information about a type parameter:
473
+
474
+ ```scala
475
+ final case class TypeParam(
476
+ name: String, // "A", "T", "K", "F"
477
+ index: Int, // Position: 0, 1, 2, ...
478
+ variance: Variance = Variance.Invariant, // +, -, or none
479
+ bounds: TypeBounds = TypeBounds.Unbounded, // >: Lower <: Upper
480
+ kind: Kind = Kind.Type // *, * -> *, etc.
481
+ )
385
482
  ```
386
483
 
387
- ## Namespaces and Type Names
484
+ To inspect individual fields of a type parameter, we can extract and examine each one:
485
+
486
+ ```scala
487
+ import zio.blocks.typeid._
388
488
 
389
- ### Owner
489
+ sealed trait Functor[F[_]]
490
+ ```
390
491
 
391
- `Owner` represents where a type is defined in the package hierarchy:
492
+ To inspect individual fields of a type parameter, extract and examine each property:
392
493
 
393
494
  ```scala
394
- // From package path
395
- val owner = Owner.fromPackagePath("com.example.app")
396
- // Owner(List(Package("com"), Package("example"), Package("app")))
495
+ val functorId = TypeId.of[Functor]
496
+ // functorId: TypeId[[F >: Nothing <: [_$1 >: Nothing <: Any] =>> Any] =>> Functor[F]] = Functor[F]
497
+ val paramF = functorId.typeParams.head
498
+ // paramF: TypeParam = TypeParam(
499
+ // name = "F",
500
+ // index = 0,
501
+ // variance = Invariant,
502
+ // bounds = TypeBounds(lower = None, upper = None),
503
+ // kind = Type
504
+ // )
505
+
506
+ paramF.name
507
+ // res42: String = "F"
508
+ paramF.index
509
+ // res43: Int = 0
510
+ paramF.variance
511
+ // res44: Variance = Invariant
512
+ paramF.isInvariant
513
+ // res45: Boolean = true
514
+ paramF.kind
515
+ // res46: Kind = Type
516
+ paramF.isTypeConstructor
517
+ // res47: Boolean = false
518
+ ```
397
519
 
398
- // Build incrementally
399
- val owner = Owner.Root / "com" / "example"
520
+ `TypeParam` provides convenience predicates for checking variance without inspecting the raw `variance` field:
400
521
 
401
- // Add term (object) segment
402
- val owner = (Owner.Root / "com").term("MyObject")
522
+ ```scala
523
+ import zio.blocks.typeid._
403
524
 
404
- // Add type segment
405
- val owner = (Owner.Root / "com").tpe("MyClass")
525
+ sealed trait Box[+A]
526
+ sealed trait Sink[-T]
527
+ sealed trait Cache[K, +V]
406
528
  ```
407
529
 
408
- Owner properties:
530
+ Using these types, we can check the variance predicates to verify which parameters are covariant, contravariant, or invariant:
409
531
 
410
532
  ```scala
411
- owner.asString // "com.example" - dot-separated path
412
- owner.isRoot // true if empty
413
- owner.parent // Parent owner (or Root)
414
- owner.lastName // Last segment name
533
+ val boxId = TypeId.of[Box]
534
+ // boxId: TypeId[[A >: Nothing <: Any] =>> Box[A]] = Box[+A]
535
+ boxId.typeParams.head.isCovariant
536
+ // res49: Boolean = true
537
+
538
+ val sinkId = TypeId.of[Sink]
539
+ // sinkId: TypeId[[T >: Nothing <: Any] =>> Sink[T]] = Sink[-T]
540
+ sinkId.typeParams.head.isContravariant
541
+ // res50: Boolean = true
542
+
543
+ val cacheId = TypeId.of[Cache]
544
+ // cacheId: TypeId[[K >: Nothing <: Any, V >: Nothing <: Any] =>> Cache[K, V]] = Cache[K, +V]
545
+ val (k, v) = (cacheId.typeParams(0), cacheId.typeParams(1))
546
+ // k: TypeParam = TypeParam(
547
+ // name = "K",
548
+ // index = 0,
549
+ // variance = Invariant,
550
+ // bounds = TypeBounds(lower = None, upper = None),
551
+ // kind = Type
552
+ // )
553
+ // v: TypeParam = TypeParam(
554
+ // name = "V",
555
+ // index = 1,
556
+ // variance = Covariant,
557
+ // bounds = TypeBounds(lower = None, upper = None),
558
+ // kind = Type
559
+ // )
560
+ k.isInvariant
561
+ // res51: Boolean = true
562
+ v.isCovariant
563
+ // res52: Boolean = true
415
564
  ```
416
565
 
417
- ### Predefined Owners
566
+ #### `typeArgs` — Applied Type Arguments
418
567
 
419
- TypeId provides common namespaces:
568
+ **Applied types** are generic types instantiated with concrete type arguments. For example, `List[Int]` is the generic `List` type constructor applied to the `Int` type argument, and `Map[String, Int]` applies two arguments to `Map`. When you derive a `TypeId` for an applied type, the `typeArgs` method returns the concrete type arguments as a list of `TypeRepr` values — allowing you to inspect what types were plugged into the type constructor.
569
+
570
+ The `typeArgs` method is essential for schema systems and code generators that need to understand the full type structure. For instance, a serializer might need to know that `List[Int]` has `Int` as its element type, or a validator might need to distinguish between `Map[String, Int]` and `Map[String, String]`:
420
571
 
421
572
  ```scala
422
- Owner.scala // scala
423
- Owner.scalaUtil // scala.util
424
- Owner.scalaCollectionImmutable // scala.collection.immutable
425
- Owner.javaLang // java.lang
426
- Owner.javaTime // java.time
427
- Owner.javaUtil // java.util
573
+ sealed trait TypeId[A <: AnyKind] {
574
+ def typeArgs: List[TypeRepr]
575
+ }
428
576
  ```
429
577
 
430
- ### TermPath
578
+ `typeArgs` returns a list of `TypeRepr` values. `TypeRepr` is an algebraic data type that represents type expressions in the Scala type system — these can be simple type references (like `Int` or `String`), complex applied types (like `List[String]`), or compound types (like `A & B`). For a detailed breakdown of all `TypeRepr` variants, see [TypeRepr — Type Expressions](#typerepr--type-expressions).
431
579
 
432
- `TermPath` represents paths to term values (for singleton types):
580
+ Setup some custom generic types with different type argument patterns:
433
581
 
434
582
  ```scala
435
- // com.example.MyObject.value.type
436
- val path = TermPath.fromOwner(
437
- Owner.fromPackagePath("com.example").term("MyObject"),
438
- "value"
439
- )
583
+ import zio.blocks.typeid._
440
584
 
441
- path.asString // "com.example.MyObject.value"
442
- path.isEmpty // false
443
- path / "nested" // Append segment
585
+ case class Pair[A, B](first: A, second: B)
586
+ case class Container[T](value: T)
587
+ case class Result[E, V](error: Option[E], value: Option[V])
444
588
  ```
445
589
 
446
- ## Type Parameters
590
+ Now inspect the type arguments of various applied types:
447
591
 
448
- ### TypeParam
592
+ ```scala
593
+ // Simple single type argument
594
+ val containerIntId = TypeId.of[Container[Int]]
595
+ // containerIntId: TypeId[Container[Int]] = Container[Int]
596
+ containerIntId.typeArgs
597
+ // res54: List[TypeRepr] = List(Ref(Int))
598
+
599
+ // Multiple type arguments
600
+ val pairId = TypeId.of[Pair[String, Double]]
601
+ // pairId: TypeId[Pair[String, Double]] = Pair[String, Double]
602
+ pairId.typeArgs
603
+ // res55: List[TypeRepr] = List(Ref(String), Ref(Double))
604
+
605
+ // Nested applied types
606
+ val resultId = TypeId.of[Result[String, List[Int]]]
607
+ // resultId: TypeId[Result[String, List[Int]]] = Result[String, List[Int]]
608
+ resultId.typeArgs
609
+ // res56: List[TypeRepr] = List(
610
+ // Ref(String),
611
+ // Applied(tycon = Ref(List[+A]), args = List(Ref(Int)))
612
+ // )
613
+
614
+ // Type constructor with no arguments has empty typeArgs
615
+ TypeId.of[List].typeArgs
616
+ // res57: List[TypeRepr] = List()
617
+ ```
449
618
 
450
- Represents a type parameter specification:
619
+ When you access `typeArgs`, each element is a `TypeRepr` describing that argument. You can inspect them further to understand the structure:
451
620
 
452
621
  ```scala
453
- // Basic type parameter
454
- TypeParam("A", index = 0)
622
+ val mapStringIntId = TypeId.of[Map[String, Int]]
623
+ // mapStringIntId: TypeId[Map[String, Int]] = Map[String, Int]
624
+ val args = mapStringIntId.typeArgs
625
+ // args: List[TypeRepr] = List(Ref(String), Ref(Int))
626
+
627
+ // First argument: String
628
+ args(0)
629
+ // res58: TypeRepr = Ref(String)
630
+
631
+ // Second argument: Int
632
+ args(1)
633
+ // res59: TypeRepr = Ref(Int)
634
+ ```
455
635
 
456
- // Covariant (+A)
457
- TypeParam("A", 0, Variance.Covariant)
458
- TypeParam.covariant("A", 0)
636
+ For more complex types, `typeArgs` captures the full structure of the arguments, including unions, intersections, function types, and tuples:
459
637
 
460
- // Contravariant (-A)
461
- TypeParam("A", 0, Variance.Contravariant)
462
- TypeParam.contravariant("A", 0)
638
+ ```scala
639
+ import zio.blocks.typeid._
463
640
 
464
- // With bounds (A <: Upper)
465
- TypeParam.bounded("A", 0, upper = TypeRepr.Ref(upperType))
641
+ // Union types (Scala 3)
642
+ case class Handler[T](process: T | String)
466
643
 
467
- // Higher-kinded (F[_])
468
- TypeParam.higherKinded("F", 0, arity = 1)
469
- TypeParam("F", 0, kind = Kind.Star1)
644
+ // Intersection types (Scala 3)
645
+ trait Readable { def read(): String }
646
+ trait Writable { def write(data: String): Unit }
647
+ case class Stream[T](data: T & Readable & Writable)
470
648
 
471
- // Full specification
472
- TypeParam(
473
- name = "A",
474
- index = 0,
475
- variance = Variance.Covariant,
476
- bounds = TypeBounds.upper(someType),
477
- kind = Kind.Type
478
- )
649
+ // Function type arguments
650
+ case class Transformer[A, B](f: A => B)
651
+
652
+ // Tuple type arguments
653
+ case class MultiValue[A, B, C](values: (A, B, C))
479
654
  ```
480
655
 
481
- TypeParam properties:
656
+ Now inspect the type arguments in these complex types:
482
657
 
483
658
  ```scala
484
- param.name // "A"
485
- param.index // Position in parameter list
486
- param.variance // Covariant, Contravariant, or Invariant
487
- param.bounds // TypeBounds
488
- param.kind // Kind (*, * -> *, etc.)
489
-
490
- param.isCovariant // variance == Covariant
491
- param.isContravariant // variance == Contravariant
492
- param.isInvariant // variance == Invariant
493
- param.hasUpperBound // bounds.upper.isDefined
494
- param.hasLowerBound // bounds.lower.isDefined
495
- param.isProperType // kind == Kind.Type
496
- param.isTypeConstructor // kind != Kind.Type
659
+ // Union type argument
660
+ val handlerStrId = TypeId.of[Handler[Int]]
661
+ // handlerStrId: TypeId[Handler[Int]] = Handler[Int]
662
+ handlerStrId.typeArgs
663
+ // res61: List[TypeRepr] = List(Ref(Int))
664
+
665
+ // Intersection type argument
666
+ val streamId = TypeId.of[Stream[List[String]]]
667
+ // streamId: TypeId[Stream[List[String]]] = Stream[List[String]]
668
+ streamId.typeArgs
669
+ // res62: List[TypeRepr] = List(
670
+ // Applied(tycon = Ref(List[+A]), args = List(Ref(String)))
671
+ // )
672
+
673
+ // Function type as argument
674
+ val transformerId = TypeId.of[Transformer[String, Int]]
675
+ // transformerId: TypeId[Transformer[String, Int]] = Transformer[String, Int]
676
+ transformerId.typeArgs
677
+ // res63: List[TypeRepr] = List(Ref(String), Ref(Int))
678
+
679
+ // Tuple type as argument
680
+ val multiValueId = TypeId.of[MultiValue[String, Int, Boolean]]
681
+ // multiValueId: TypeId[MultiValue[String, Int, Boolean]] = MultiValue[String, Int, Boolean]
682
+ multiValueId.typeArgs
683
+ // res64: List[TypeRepr] = List(Ref(String), Ref(Int), Ref(Boolean))
497
684
  ```
498
685
 
499
- ### TypeBounds
686
+ #### `arity` — Number of Type Parameters
500
687
 
501
- Represents type parameter bounds:
688
+ The **arity** of a type is the number of formal type parameters it declares. A type with arity 0 is fully applied (a "proper type"), while arity > 0 means it's a type constructor that needs to be instantiated with type arguments. Arity is useful for generic programming and type-indexed registries where you need to distinguish between different levels of type abstraction:
502
689
 
503
690
  ```scala
504
- // No bounds (>: Nothing <: Any)
505
- TypeBounds.Unbounded
506
-
507
- // Upper bound only (<: Upper)
508
- TypeBounds.upper(upperType)
691
+ sealed trait TypeId[A <: AnyKind] {
692
+ def arity: Int
693
+ }
694
+ ```
509
695
 
510
- // Lower bound only (>: Lower)
511
- TypeBounds.lower(lowerType)
696
+ Setup some generic types with different arities:
512
697
 
513
- // Both bounds (>: Lower <: Upper)
514
- TypeBounds(lowerType, upperType)
698
+ ```scala
699
+ import zio.blocks.typeid._
515
700
 
516
- // Type alias bounds (lower == upper)
517
- TypeBounds.alias(aliasType)
701
+ case class Single[A](value: A) // Arity 1
702
+ case class Pair[A, B](a: A, b: B) // Arity 2
703
+ case class Triple[A, B, C](a: A, b: B, c: C) // Arity 3
704
+ case class Value(x: Int) // Arity 0
518
705
  ```
519
706
 
520
- TypeBounds properties:
707
+ Check the arity of different types:
521
708
 
522
709
  ```scala
523
- bounds.lower // Option[TypeRepr]
524
- bounds.upper // Option[TypeRepr]
525
- bounds.isUnbounded // No bounds specified
526
- bounds.hasOnlyUpper // Only upper bound
527
- bounds.hasOnlyLower // Only lower bound
528
- bounds.hasBothBounds // Both bounds specified
529
- bounds.isAlias // lower == upper
530
- bounds.aliasType // Option[TypeRepr] if alias
710
+ TypeId.of[Single].arity
711
+ // res66: Int = 1
712
+ TypeId.of[Pair].arity
713
+ // res67: Int = 2
714
+ TypeId.of[Triple].arity
715
+ // res68: Int = 3
716
+ TypeId.of[Value].arity
717
+ // res69: Int = 0
718
+
719
+ // Applied types have the same arity as their type constructor
720
+ TypeId.of[Single[Int]].arity
721
+ // res70: Int = 1
722
+ TypeId.of[Pair[String, Int]].arity
723
+ // res71: Int = 2
531
724
  ```
532
725
 
533
- ### Variance
726
+ #### `isProperType` — Has No Type Parameters
727
+
728
+ A **proper type** (also called a ground type or monomorphic type) is a fully instantiated type with no unresolved type parameters. It's the opposite of a type constructor — you can directly instantiate values of a proper type, whereas a type constructor needs type arguments before it's usable. The `isProperType` predicate returns `true` when `arity == 0`, helping distinguish concrete types from abstract type constructors:
534
729
 
535
730
  ```scala
536
- Variance.Covariant // +A
537
- Variance.Contravariant // -A
538
- Variance.Invariant // A
731
+ import zio.blocks.typeid._
539
732
 
540
- variance.symbol // "+", "-", or ""
541
- variance.isCovariant
542
- variance.isContravariant
543
- variance.isInvariant
544
- variance.flip // Covariant <-> Contravariant
545
- variance * other // Combine variances
733
+ case class Single[A](value: A)
734
+ case class Pair[A, B](a: A, b: B)
735
+ case class Value(x: Int)
546
736
  ```
547
737
 
548
- ### Kind
549
-
550
- Represents the "kind" of a type (type of types):
738
+ Check which types are proper types:
551
739
 
552
740
  ```scala
553
- Kind.Type // * (proper type like Int, String)
554
- Kind.Star // Alias for Type
555
- Kind.Star1 // * -> * (List, Option)
556
- Kind.Star2 // * -> * -> * (Map, Either)
557
- Kind.HigherStar1 // (* -> *) -> * (Functor, Monad)
741
+ // Proper types: fully instantiated, arity == 0
742
+ TypeId.of[Value].isProperType
743
+ // res73: Boolean = true
744
+ TypeId.of[List[Int]].isProperType
745
+ // res74: Boolean = false
746
+ TypeId.of[Pair[String, Int]].isProperType
747
+ // res75: Boolean = false
748
+ TypeId.of[Int].isProperType
749
+ // res76: Boolean = true
750
+
751
+ // Type constructors: need type arguments, arity > 0
752
+ TypeId.of[Single].isProperType
753
+ // res77: Boolean = false
754
+ TypeId.of[Pair].isProperType
755
+ // res78: Boolean = false
756
+ TypeId.of[List].isProperType
757
+ // res79: Boolean = false
758
+ ```
558
759
 
559
- Kind.constructor(0) // *
560
- Kind.constructor(1) // * -> *
561
- Kind.constructor(2) // * -> * -> *
760
+ #### `isTypeConstructor` — Has Type Parameters
562
761
 
563
- // Custom kinds
564
- Kind.Arrow(List(Kind.Type), Kind.Type) // * -> *
565
- Kind.Arrow(List(Kind.Star1), Kind.Type) // (* -> *) -> *
762
+ A **type constructor** is a parameterized type that cannot be instantiated directly — it requires concrete type arguments first. For example, `List` is a type constructor (you can't have a value of type `List`, only `List[Int]` or `List[String]`). The `isTypeConstructor` predicate returns `true` when `arity > 0`, indicating the type needs to be applied with arguments before use. This is useful for generic programming where you work with families of related types:
763
+
764
+ ```scala
765
+ import zio.blocks.typeid._
766
+
767
+ case class Single[A](value: A)
768
+ case class Pair[A, B](a: A, b: B)
769
+ case class Value(x: Int)
566
770
  ```
567
771
 
568
- Kind properties:
772
+ Identify which types are type constructors:
569
773
 
570
774
  ```scala
571
- kind.isProperType // kind == Kind.Type
572
- kind.arity // Number of type parameters
775
+ // Type constructors: need type arguments, arity > 0
776
+ TypeId.of[Single].isTypeConstructor
777
+ // res81: Boolean = true
778
+ TypeId.of[Pair].isTypeConstructor
779
+ // res82: Boolean = true
780
+ TypeId.of[List].isTypeConstructor
781
+ // res83: Boolean = true
782
+ TypeId.of[Map].isTypeConstructor
783
+ // res84: Boolean = true
784
+
785
+ // Proper types: fully instantiated, no type parameters
786
+ TypeId.of[Value].isTypeConstructor
787
+ // res85: Boolean = false
788
+ TypeId.of[List[Int]].isTypeConstructor
789
+ // res86: Boolean = true
790
+ TypeId.of[Int].isTypeConstructor
791
+ // res87: Boolean = false
573
792
  ```
574
793
 
575
- ## Members (Structural Types)
794
+ #### `isApplied` — Has Type Arguments
576
795
 
577
- ### Val/Var Members
796
+ An **applied type** is a generic type that has been instantiated with concrete type arguments. For example, `List[Int]` is an applied type (`List` applied to `Int`), while `List` by itself is a type constructor with no arguments applied. The `isApplied` predicate returns `true` when `typeArgs.nonEmpty`, helping distinguish between abstract type constructors and concrete instantiated types. This is useful for code generators that need to know whether a type is ready for use:
578
797
 
579
798
  ```scala
580
- // val x: Int
581
- Member.Val("x", TypeRepr.Ref(TypeId.int))
799
+ import zio.blocks.typeid._
582
800
 
583
- // var y: String
584
- Member.Val("y", TypeRepr.Ref(TypeId.string), isVar = true)
801
+ case class Single[A](value: A)
802
+ case class Pair[A, B](a: A, b: B)
585
803
  ```
586
804
 
587
- ### Method Members
805
+ Check which types are applied:
588
806
 
589
807
  ```scala
590
- // def foo: Int
591
- Member.Def("foo", Nil, Nil, TypeRepr.Ref(TypeId.int))
808
+ // Applied types: have type arguments
809
+ TypeId.of[List[Int]].isApplied
810
+ // res89: Boolean = true
811
+ TypeId.of[Pair[String, Int]].isApplied
812
+ // res90: Boolean = true
813
+ TypeId.of[Single[Boolean]].isApplied
814
+ // res91: Boolean = true
815
+ TypeId.of[Map[String, Double]].isApplied
816
+ // res92: Boolean = true
817
+
818
+ // Type constructors: no type arguments applied
819
+ TypeId.of[List].isApplied
820
+ // res93: Boolean = false
821
+ TypeId.of[Single].isApplied
822
+ // res94: Boolean = false
823
+ TypeId.of[Pair].isApplied
824
+ // res95: Boolean = false
825
+ TypeId.of[Map].isApplied
826
+ // res96: Boolean = false
827
+ ```
592
828
 
593
- // def bar(x: Int): String
594
- Member.Def(
595
- name = "bar",
596
- typeParams = Nil,
597
- paramLists = List(List(Param("x", TypeRepr.Ref(TypeId.int)))),
598
- result = TypeRepr.Ref(TypeId.string)
599
- )
829
+ ### Type Classification
600
830
 
601
- // def baz[A](x: A)(implicit y: Ordering[A]): List[A]
602
- Member.Def(
603
- name = "baz",
604
- typeParams = List(TypeParam.A),
605
- paramLists = List(
606
- List(Param("x", TypeRepr.ParamRef(TypeParam.A))),
607
- List(Param("y", orderingA, isImplicit = true))
608
- ),
609
- result = listA
610
- )
831
+ **Type classification** determines what kind of type definition something is — whether it's a class, trait, object, enum, alias, opaque type, or something else. This is essential for code generators, serializers, and frameworks that need to handle different type categories differently. TypeId provides both a `defKind` property that returns detailed classification information, and convenient predicates (like `isClass`, `isTrait`, `isCaseClass`) for common checks.
832
+
833
+ #### `defKind` — Type Definition Kind
834
+
835
+ Returns the `TypeDefKind` classifying this type (class, trait, object, enum, alias, opaque, etc.):
836
+
837
+ ```scala
838
+ sealed trait TypeId[A <: AnyKind] {
839
+ def defKind: TypeDefKind
840
+ }
611
841
  ```
612
842
 
613
- ### Type Members
843
+ Define types representing different classifications:
614
844
 
615
845
  ```scala
616
- // type T
617
- Member.TypeMember("T")
846
+ import zio.blocks.typeid._
847
+
848
+ sealed trait Animal
849
+ case class Dog(name: String) extends Animal
850
+ case object Sentinel
851
+ type UserId = String
852
+ opaque type Email = String
853
+ enum Color { case Red; case Green; case Blue }
854
+ ```
618
855
 
619
- // type T <: Upper
620
- Member.TypeMember("T", upperBound = Some(upperType))
856
+ Inspect the `defKind` for each type to see how they're classified:
621
857
 
622
- // type T = Alias (isAlias when lower == upper)
623
- Member.TypeMember("T",
624
- lowerBound = Some(aliasType),
625
- upperBound = Some(aliasType)
626
- )
858
+ ```scala
859
+ TypeId.of[Dog].defKind
860
+ // res98: TypeDefKind = Class(
861
+ // isFinal = false,
862
+ // isAbstract = false,
863
+ // isCase = true,
864
+ // isValue = false,
865
+ // bases = List(Ref(Animal))
866
+ // )
867
+ TypeId.of[Animal].defKind
868
+ // res99: TypeDefKind = Trait(isSealed = true, bases = List())
869
+ TypeId.of[Sentinel.type].defKind
870
+ // res100: TypeDefKind = Object(List())
871
+ TypeId.of[UserId].defKind
872
+ // res101: TypeDefKind = TypeAlias
873
+ TypeId.of[Email].defKind
874
+ // res102: TypeDefKind = OpaqueType(TypeBounds(lower = None, upper = None))
875
+ TypeId.of[Color].defKind
876
+ // res103: TypeDefKind = Enum(List(Ref(Enum)))
627
877
  ```
628
878
 
629
- ## TypeDefKind
879
+ #### Classification Predicates
880
+
881
+ Each predicate inspects `defKind` for a specific type definition kind. These are convenience methods that save you from pattern matching on `defKind` directly:
630
882
 
631
- Classifies what kind of type definition a TypeId represents:
883
+ | Predicate | Returns `true` when |
884
+ |------------------|-------------------------------------------------------------|
885
+ | `isClass` | `defKind` is `TypeDefKind.Class` |
886
+ | `isTrait` | `defKind` is `TypeDefKind.Trait` |
887
+ | `isObject` | `defKind` is `TypeDefKind.Object` |
888
+ | `isEnum` | `defKind` is `TypeDefKind.Enum` |
889
+ | `isAlias` | `defKind` is `TypeDefKind.TypeAlias` |
890
+ | `isOpaque` | `defKind` is `TypeDefKind.OpaqueType` |
891
+ | `isAbstract` | `defKind` is `TypeDefKind.AbstractType` |
892
+ | `isSealed` | `defKind` is `TypeDefKind.Trait(isSealed = true, _)` (sealed traits only) |
893
+ | `isCaseClass` | `defKind` is `TypeDefKind.Class(_, _, isCase = true, _, _)` |
894
+ | `isValueClass` | `defKind` is `TypeDefKind.Class(_, _, _, isValue = true, _)`|
632
895
 
633
- ### Class
896
+ Use the classification predicates to identify type kinds:
634
897
 
635
898
  ```scala
636
- TypeDefKind.Class(
637
- isFinal = false,
638
- isAbstract = false,
639
- isCase = true, // case class
640
- isValue = false, // extends AnyVal
641
- bases = List(...) // parent types
642
- )
899
+ val animalId = TypeId.of[Animal]
900
+ // animalId: TypeId[Animal] = Animal
901
+ val dogId = TypeId.of[Dog]
902
+ // dogId: TypeId[Dog] = Dog
903
+ val sentinelId = TypeId.of[Sentinel.type]
904
+ // sentinelId: TypeId[Sentinel] = Sentinel
905
+ val userIdId = TypeId.of[UserId]
906
+ // userIdId: TypeId[UserId] = UserId
907
+ val emailId = TypeId.of[Email]
908
+ // emailId: TypeId[Email] = Email
909
+ val colorId = TypeId.of[Color]
910
+ // colorId: TypeId[Color] = Color
911
+
912
+ // Trait classifications
913
+ animalId.isTrait
914
+ // res104: Boolean = true
915
+ animalId.isSealed
916
+ // res105: Boolean = true
917
+
918
+ // Case class
919
+ dogId.isCaseClass
920
+ // res106: Boolean = true
921
+
922
+ // Object/Singleton
923
+ sentinelId.isObject
924
+ // res107: Boolean = true
925
+
926
+ // Type alias
927
+ userIdId.isAlias
928
+ // res108: Boolean = true
929
+
930
+ // Opaque type
931
+ emailId.isOpaque
932
+ // res109: Boolean = true
933
+
934
+ // Enum
935
+ colorId.isEnum
936
+ // res110: Boolean = true
937
+ ```
938
+
939
+ :::note
940
+ `isSealed` only checks sealed traits. A sealed abstract class or sealed enum will return `false`; use `defKind` directly to inspect those cases by pattern matching on `TypeDefKind.Class(_, isAbstract = true, ...)` or `TypeDefKind.Enum(...)`.
941
+ :::
942
+
943
+ #### Semantic Predicates
944
+
945
+ **Semantic predicates** check specific semantic properties of the type after normalization, allowing you to identify built-in Scala types like tuples, products, sums, options, and either. These are useful for generic serializers and validators that treat built-in types specially.
946
+
947
+ **Normalization** resolves type aliases and opaque types to their underlying representations. For example, if you have `type UserId = String`, normalization reveals that the underlying type is `String`. Similarly, an opaque type like `opaque type Email = String` normalizes to `String`. This allows predicates like `isOption` to work correctly even when the type is wrapped in an alias or opaque type — it will look through the wrapper to find the actual semantic type.
948
+
949
+ | Predicate | Checks |
950
+ |-------------|--------------------------------------------------------|
951
+ | `isTuple` | Normalized type is `scala.TupleN` |
952
+ | `isProduct` | Normalized type is `scala.Product` or `scala.ProductN` |
953
+ | `isSum` | Normalized type is named `Either` or `Option` |
954
+ | `isEither` | Normalized type is `scala.util.Either` |
955
+ | `isOption` | Normalized type is `scala.Option` |
956
+
957
+ Check semantic properties with practical examples:
958
+
959
+ ```scala
960
+ // Tuples
961
+ TypeId.of[(String, Int)].isTuple
962
+ // res111: Boolean = true
963
+ TypeId.of[(Int, String, Boolean)].isTuple
964
+ // res112: Boolean = true
965
+
966
+ // Options and Either
967
+ TypeId.of[Option[String]].isOption
968
+ // res113: Boolean = true
969
+ TypeId.of[Either[String, Int]].isEither
970
+ // res114: Boolean = true
971
+
972
+ // Products (built-in Scala Product interface, not user case classes)
973
+ TypeId.of[Product2[String, Int]].isProduct
974
+ // res115: Boolean = true
643
975
  ```
644
976
 
645
- ### Trait
977
+ :::note
978
+ `isProduct` returns `true` only for Scala's built-in `scala.Product`, `scala.Product1`, etc. -- not for user-defined case classes. Use `isCaseClass` for that.
979
+ :::
980
+
981
+ Understanding the distinction between `isSum`, `isEither`, and `isOption`:
646
982
 
647
983
  ```scala
648
- TypeDefKind.Trait(
649
- isSealed = true,
650
- bases = List(...)
651
- )
984
+ import zio.blocks.typeid._
985
+ // For standard library types, use isEither and isOption
986
+ TypeId.of[Option[String]].isOption
987
+ // res116: Boolean = true
988
+ TypeId.of[Option[String]].isSum
989
+ // res117: Boolean = true
990
+
991
+ TypeId.of[Either[String, Int]].isEither
992
+ // res118: Boolean = true
993
+ TypeId.of[Either[String, Int]].isSum
994
+ // res119: Boolean = false
652
995
  ```
653
996
 
654
- ### Object
997
+ [//]: # (:::note)
998
+ [//]: # (**Practical guidance:** Always use `isEither` for `scala.util.Either` and `isOption` for `scala.Option`. The `isSum` predicate is rarely needed — it checks for hypothetical types named `"Option"` or `"Either"` placed directly in the `scala` package itself &#40;not in `scala.util` subpackage&#41;, which almost never occurs in real code.)
999
+ [//]: # (:::)
1000
+
1001
+ ### Subtype Relationships
1002
+
1003
+ **Subtype relationships** determine the inheritance hierarchy and compatibility between types at runtime. This is essential for type-safe dispatch, generic programming, and validating that a value of one type can be used where another type is expected. TypeId provides methods to check direct and transitive subtyping, supertyping, type equivalence, and inspect the parent types in the hierarchy.
1004
+
1005
+ #### `isSubtypeOf` — Check Subtyping
1006
+
1007
+ Checks if this type is a subtype of another type. A type is a subtype if it extends or implements the other type, either directly or transitively. This method handles direct inheritance, sealed trait subtypes, enum cases, transitive inheritance chains, and variance-aware subtyping for applied generic types:
655
1008
 
656
1009
  ```scala
657
- TypeDefKind.Object(
658
- bases = List(...)
659
- )
1010
+ sealed trait TypeId[A <: AnyKind] {
1011
+ def isSubtypeOf(other: TypeId[?]): Boolean
1012
+ }
660
1013
  ```
661
1014
 
662
- ### Enum (Scala 3)
1015
+ Define a type hierarchy with direct and transitive relationships:
663
1016
 
664
1017
  ```scala
665
- TypeDefKind.Enum(
666
- bases = List(...)
667
- )
1018
+ import zio.blocks.typeid._
668
1019
 
669
- TypeDefKind.EnumCase(
670
- parentEnum = parentEnumRef,
671
- ordinal = 0,
672
- isObjectCase = true
673
- )
1020
+ sealed trait Animal
1021
+ sealed trait Mammal extends Animal
1022
+ case class Dog(name: String) extends Mammal
1023
+ case class Cat(name: String) extends Mammal
1024
+ case class Fish(species: String) extends Animal
674
1025
  ```
675
1026
 
676
- ### Type Aliases and Opaque Types
1027
+ Check subtyping relationships:
677
1028
 
678
1029
  ```scala
679
- TypeDefKind.TypeAlias // type Foo = Bar
1030
+ val dogId = TypeId.of[Dog]
1031
+ // dogId: TypeId[Dog] = Dog
1032
+ val mammalId = TypeId.of[Mammal]
1033
+ // mammalId: TypeId[Mammal] = Mammal
1034
+ val animalId = TypeId.of[Animal]
1035
+ // animalId: TypeId[Animal] = Animal
1036
+ val fishId = TypeId.of[Fish]
1037
+ // fishId: TypeId[Fish] = Fish
1038
+
1039
+ // Direct inheritance: Dog extends Mammal
1040
+ dogId.isSubtypeOf(mammalId)
1041
+ // res121: Boolean = true
1042
+
1043
+ // Transitive inheritance: Dog extends Mammal extends Animal
1044
+ dogId.isSubtypeOf(animalId)
1045
+ // res122: Boolean = true
1046
+
1047
+ // Not a subtype relationship
1048
+ dogId.isSubtypeOf(fishId)
1049
+ // res123: Boolean = false
1050
+ fishId.isSubtypeOf(mammalId)
1051
+ // res124: Boolean = false
1052
+ ```
680
1053
 
681
- TypeDefKind.OpaqueType(
682
- publicBounds = TypeBounds.Unbounded // Bounds visible outside
683
- )
1054
+ Covariant type constructors preserve subtyping relationships:
684
1055
 
685
- TypeDefKind.AbstractType // Abstract type member
1056
+ ```scala
1057
+ TypeId.of[List[Dog]].isSubtypeOf(TypeId.of[List[Mammal]])
1058
+ // res125: Boolean = true
1059
+ TypeId.of[List[Dog]].isSubtypeOf(TypeId.of[List[Animal]])
1060
+ // res126: Boolean = true
686
1061
  ```
687
1062
 
688
- ## Annotations
689
-
690
- Represent Scala/Java annotations attached to types:
1063
+ **Scala 3 exclusive features:** In Scala 3, `isSubtypeOf` handles advanced type relationships that Scala 2 cannot. These examples show what works in Scala 3:
691
1064
 
692
1065
  ```scala
693
- Annotation(
694
- typeId = TypeId.of[deprecated],
695
- args = List(
696
- AnnotationArg.Named("message",
697
- AnnotationArg.Const("use newMethod")),
698
- AnnotationArg.Named("since",
699
- AnnotationArg.Const("1.0"))
700
- )
701
- )
1066
+ import zio.blocks.typeid._
1067
+
1068
+ // Scala 3: Enum cases
1069
+ enum Color {
1070
+ case Red
1071
+ case Green
1072
+ case Blue
1073
+ }
1074
+
1075
+ // Scala 3: Union type aliases
1076
+ type StringOrInt = String | Int
1077
+
1078
+ // Scala 3: Intersection type aliases
1079
+ trait Readable { def read(): String }
1080
+ trait Writable { def write(data: String): Unit }
1081
+ type ReadWrite = Readable & Writable
702
1082
  ```
703
1083
 
704
- Annotation argument types:
1084
+ In Scala 3, `isSubtypeOf` correctly handles these advanced type cases:
705
1085
 
706
1086
  ```scala
707
- AnnotationArg.Const(value) // Constant value
708
- AnnotationArg.ArrayArg(values) // Array of args
709
- AnnotationArg.Named(name, value) // Named parameter
710
- AnnotationArg.Nested(annotation) // Nested annotation
711
- AnnotationArg.ClassOf(typeRepr) // classOf[T]
712
- AnnotationArg.EnumValue(enumType, valueName) // Enum constant
1087
+ // Enum cases: Red is a subtype of Color
1088
+ TypeId.of[Color.Red.type].isSubtypeOf(TypeId.of[Color])
1089
+ // res128: Boolean = true
1090
+
1091
+ // Union type aliases: String is one of the union members
1092
+ TypeId.of[String].isSubtypeOf(TypeId.of[StringOrInt])
1093
+ // res129: Boolean = true
1094
+ TypeId.of[Int].isSubtypeOf(TypeId.of[StringOrInt])
1095
+ // res130: Boolean = true
1096
+
1097
+ // Intersection type aliases: A type implementing both traits is a subtype
1098
+ val readWriteId = TypeId.of[ReadWrite]
1099
+ // readWriteId: TypeId[ReadWrite] = ReadWrite
1100
+ val readableId = TypeId.of[Readable]
1101
+ // readableId: TypeId[Readable] = Readable
1102
+ readWriteId.isSubtypeOf(readableId)
1103
+ // res131: Boolean = true
713
1104
  ```
714
1105
 
715
- ## Predefined TypeIds
1106
+ :::note
1107
+ In Scala 2, `isSubtypeOf` does not handle `EnumCase` subtypes, types aliased to union types, or types aliased to intersection types — only the Scala 3 implementation checks those cases.
1108
+ :::
716
1109
 
717
- TypeId provides instances for common types:
1110
+ #### `isSupertypeOf` — Check Supertyping
718
1111
 
719
- ### Primitives
1112
+ The mirror of `isSubtypeOf` — returns `true` if the other type is a subtype of this type. This is useful when you need to check if a type can accept instances of another type, or when validating that a container type can hold values of a more specific type:
720
1113
 
721
1114
  ```scala
722
- TypeId.unit // scala.Unit
723
- TypeId.boolean // scala.Boolean
724
- TypeId.byte // scala.Byte
725
- TypeId.short // scala.Short
726
- TypeId.int // scala.Int
727
- TypeId.long // scala.Long
728
- TypeId.float // scala.Float
729
- TypeId.double // scala.Double
730
- TypeId.char // scala.Char
731
- TypeId.string // java.lang.String
732
- TypeId.bigInt // scala.BigInt
733
- TypeId.bigDecimal // scala.BigDecimal
1115
+ sealed trait TypeId[A <: AnyKind] {
1116
+ def isSupertypeOf(other: TypeId[?]): Boolean
1117
+ }
734
1118
  ```
735
1119
 
736
- ### Collections
1120
+ Check supertyping relationships using the same hierarchy:
737
1121
 
738
1122
  ```scala
739
- TypeId.option // scala.Option
740
- TypeId.some // scala.Some
741
- TypeId.none // scala.None
742
- TypeId.list // scala.collection.immutable.List
743
- TypeId.vector // scala.collection.immutable.Vector
744
- TypeId.set // scala.collection.immutable.Set
745
- TypeId.seq // scala.collection.immutable.Seq
746
- TypeId.indexedSeq // scala.collection.immutable.IndexedSeq
747
- TypeId.map // scala.collection.immutable.Map
748
- TypeId.either // scala.util.Either
749
- TypeId.array // scala.Array
750
- TypeId.arraySeq // scala.collection.immutable.ArraySeq
751
- TypeId.chunk // zio.blocks.chunk.Chunk
1123
+ import zio.blocks.typeid._
1124
+
1125
+ sealed trait Animal
1126
+ sealed trait Mammal extends Animal
1127
+ case class Dog(name: String) extends Mammal
1128
+ case class Cat(name: String) extends Mammal
1129
+ case class Fish(species: String) extends Animal
1130
+
1131
+ val dogId = TypeId.of[Dog]
1132
+ val mammalId = TypeId.of[Mammal]
1133
+ val animalId = TypeId.of[Animal]
1134
+ val fishId = TypeId.of[Fish]
752
1135
  ```
753
1136
 
754
- ### java.time Types
1137
+ Now check supertyping relationships:
755
1138
 
756
1139
  ```scala
757
- TypeId.dayOfWeek // java.time.DayOfWeek
758
- TypeId.duration // java.time.Duration
759
- TypeId.instant // java.time.Instant
760
- TypeId.localDate // java.time.LocalDate
761
- TypeId.localDateTime // java.time.LocalDateTime
762
- TypeId.localTime // java.time.LocalTime
763
- TypeId.month // java.time.Month
764
- TypeId.monthDay // java.time.MonthDay
765
- TypeId.offsetDateTime // java.time.OffsetDateTime
766
- TypeId.offsetTime // java.time.OffsetTime
767
- TypeId.period // java.time.Period
768
- TypeId.year // java.time.Year
769
- TypeId.yearMonth // java.time.YearMonth
770
- TypeId.zoneId // java.time.ZoneId
771
- TypeId.zoneOffset // java.time.ZoneOffset
772
- TypeId.zonedDateTime // java.time.ZonedDateTime
1140
+ // Mammal is a supertype of Dog (Mammal can hold Dog instances)
1141
+ mammalId.isSupertypeOf(dogId)
1142
+ // res133: Boolean = true
1143
+
1144
+ // Animal is a supertype of both Dog and Fish (Animal is the most general)
1145
+ animalId.isSupertypeOf(dogId)
1146
+ // res134: Boolean = true
1147
+ animalId.isSupertypeOf(fishId)
1148
+ // res135: Boolean = true
1149
+
1150
+ // Mammal is a supertype of Cat too
1151
+ mammalId.isSupertypeOf(TypeId.of[Cat])
1152
+ // res136: Boolean = true
1153
+
1154
+ // But Dog is not a supertype of Mammal (can't hold all Mammals as Dogs)
1155
+ dogId.isSupertypeOf(mammalId)
1156
+ // res137: Boolean = false
1157
+
1158
+ // And Fish is not a supertype of Mammal
1159
+ fishId.isSupertypeOf(mammalId)
1160
+ // res138: Boolean = false
773
1161
  ```
774
1162
 
775
- ### java.util Types
1163
+ :::note
1164
+ **Limitation:** TypeId's subtyping checks currently do not handle contravariance in function types. In type theory, `Mammal => String` should be a supertype of `Dog => String` due to contravariance of input parameters, but `isSupertypeOf` returns `false` for function types with subtype relationships. For practical purposes, rely on `isSupertypeOf` for class and trait hierarchies rather than complex generic type relationships.
1165
+ :::
1166
+
1167
+ #### `isEquivalentTo` — Check Type Equivalence
1168
+
1169
+ Returns `true` when two types are structurally equivalent — meaning they are **mutual subtypes** of each other. In other words, both `A.isSubtypeOf(B)` and `B.isSubtypeOf(A)` must be true. Two types are equivalent when they represent the same type through different paths, or when they normalize to the same underlying type (important for type aliases and opaque types):
776
1170
 
777
1171
  ```scala
778
- TypeId.currency // java.util.Currency
779
- TypeId.uuid // java.util.UUID
1172
+ sealed trait TypeId[A <: AnyKind] {
1173
+ def isEquivalentTo(other: TypeId[?]): Boolean
1174
+ }
780
1175
  ```
781
1176
 
782
- ## Integration with Schema
783
-
784
- TypeId is central to ZIO Blocks' schema system. Every `Reflect` node has an associated TypeId:
1177
+ Check type equivalence with practical examples:
785
1178
 
786
1179
  ```scala
787
- import zio.blocks.schema._
1180
+ import zio.blocks.typeid._
788
1181
 
789
- case class Person(name: String, age: Int)
790
- object Person {
791
- implicit val schema: Schema[Person] = Schema.derived
792
- }
1182
+ sealed trait Animal
1183
+ sealed trait Mammal extends Animal
1184
+ case class Dog(name: String) extends Mammal
1185
+ case class Cat(name: String) extends Mammal
1186
+
1187
+ val dogId = TypeId.of[Dog]
1188
+ val mammalId = TypeId.of[Mammal]
1189
+ val animalId = TypeId.of[Animal]
1190
+ val catId = TypeId.of[Cat]
1191
+ ```
793
1192
 
794
- // Access TypeId from schema
795
- val reflect = Schema[Person].reflect
796
- val typeId = reflect.typeId
1193
+ Now check type equivalence:
797
1194
 
798
- typeId.name // "Person"
799
- typeId.isCaseClass // true
1195
+ ```scala
1196
+ // A type is always equivalent to itself
1197
+ dogId.isEquivalentTo(dogId)
1198
+ // res140: Boolean = true
1199
+
1200
+ // The same type referenced twice is equivalent
1201
+ val dogId2 = TypeId.of[Dog]
1202
+ // dogId2: TypeId[Dog] = Dog
1203
+ dogId.isEquivalentTo(dogId2)
1204
+ // res141: Boolean = true
1205
+
1206
+ // Different types in the hierarchy are NOT equivalent (one-way subtyping only)
1207
+ dogId.isEquivalentTo(mammalId)
1208
+ // res142: Boolean = false
1209
+ mammalId.isEquivalentTo(animalId)
1210
+ // res143: Boolean = false
1211
+
1212
+ // Cat and Dog are different types, even though both extend Mammal
1213
+ dogId.isEquivalentTo(catId)
1214
+ // res144: Boolean = false
800
1215
  ```
801
1216
 
802
- ### Schema Transformations
1217
+ Type aliases normalize to the same type, making them equivalent:
1218
+
1219
+ ```scala
1220
+ import zio.blocks.typeid._
803
1221
 
804
- TypeId is captured when transforming schemas:
1222
+ type UserId = String
1223
+ type Username = String
1224
+ ```
1225
+
1226
+ Both aliases normalize to `String`, so they are equivalent:
805
1227
 
806
1228
  ```scala
807
- case class Email(value: String)
1229
+ val userIdType = TypeId.of[UserId]
1230
+ // userIdType: TypeId[UserId] = UserId
1231
+ val usernameType = TypeId.of[Username]
1232
+ // usernameType: TypeId[Username] = Username
1233
+ val stringType = TypeId.of[String]
1234
+ // stringType: TypeId[String] = String
1235
+
1236
+ // Both type aliases are equivalent because they normalize to the same underlying type
1237
+ userIdType.isEquivalentTo(usernameType)
1238
+ // res146: Boolean = true
1239
+
1240
+ // And both are equivalent to their underlying type
1241
+ userIdType.isEquivalentTo(stringType)
1242
+ // res147: Boolean = true
1243
+ usernameType.isEquivalentTo(stringType)
1244
+ // res148: Boolean = true
1245
+ ```
808
1246
 
809
- object Email {
810
- implicit val schema: Schema[Email] = Schema[String]
811
- .transform(Email(_), _.value)
812
- .withTypeName[Email] // Sets TypeId to Email
1247
+ #### `parents` — Parent Types
1248
+
1249
+ Returns the list of parent type representations as `TypeRepr` values, flattened across the full inheritance hierarchy. Each parent is represented as a `TypeRepr` that captures the parent type, including any type arguments it might have. This is useful for code generators, serializers, and frameworks that need to understand the inheritance structure of a type:
1250
+
1251
+ ```scala
1252
+ sealed trait TypeId[A <: AnyKind] {
1253
+ def parents: List[TypeRepr]
813
1254
  }
814
1255
  ```
815
1256
 
816
- ### Schema Derivation
1257
+ ```scala
1258
+ import zio.blocks.typeid._
1259
+
1260
+ trait Swimmer { def swim(): Unit = () }
1261
+ trait Flyer { def fly(): Unit = () }
1262
+ trait Duck extends Swimmer with Flyer
1263
+ case class MallardDuck() extends Duck
1264
+ ```
817
1265
 
818
- The `Deriver` trait receives TypeId for each node:
1266
+ Parents are flattened across the full hierarchy:
819
1267
 
820
1268
  ```scala
821
- trait Deriver[TC[_]] {
822
- def deriveRecord[A](
823
- typeId: TypeId[A],
824
- fields: => Chunk[Deriver.Field[TC, A, _]],
825
- ...
826
- ): TC[A]
827
-
828
- def deriveVariant[A](
829
- typeId: TypeId[A],
830
- cases: => Chunk[Deriver.Case[TC, A, _]],
831
- ...
832
- ): TC[A]
833
-
834
- // ... other methods
835
- }
1269
+ // Duck extends Swimmer and Flyer directly
1270
+ TypeId.of[Duck].parents
1271
+ // res150: List[TypeRepr] = List(Ref(Flyer), Ref(Swimmer))
1272
+
1273
+ // MallardDuck extends Duck — parents include Duck, Swimmer, and Flyer
1274
+ TypeId.of[MallardDuck].parents
1275
+ // res151: List[TypeRepr] = List(Ref(Duck), Ref(Flyer), Ref(Swimmer))
836
1276
  ```
837
1277
 
838
- ## Type Normalization
1278
+ ### Metadata
1279
+
1280
+ Methods for accessing annotations, self-type, alias target, and opaque representation.
839
1281
 
840
- Type aliases are normalized to their underlying types for comparison:
1282
+ #### `annotations` — Type Annotations
1283
+
1284
+ Returns the list of annotations attached to this type at compile time. Each `Annotation` carries the annotation's name and its argument values, making this useful for frameworks that drive behaviour from annotations (e.g. serialization hints, validation rules, or access-control markers):
841
1285
 
842
1286
  ```scala
843
- type Age = Int
1287
+ sealed trait TypeId[A <: AnyKind] {
1288
+ def annotations: List[Annotation]
1289
+ }
1290
+ ```
844
1291
 
845
- val ageId = TypeId.alias[Age]("Age", owner, Nil, TypeRepr.Ref(TypeId.int))
846
- val normalized = TypeId.normalize(ageId)
1292
+ ```scala
1293
+ import zio.blocks.typeid._
847
1294
 
848
- normalized.fullName // "scala.Int" (not "Age")
1295
+ @deprecated("use NewData instead", "2.0")
1296
+ @transient
1297
+ case class LegacyData(id: Int, payload: String)
1298
+ case class Plain(x: Int)
849
1299
  ```
850
1300
 
851
- Normalization handles nested aliases and type arguments:
1301
+ A type with annotations reports each annotation by name; an unannotated type returns an empty list:
852
1302
 
853
1303
  ```scala
854
- type IntList = List[Int]
855
- type MyIntList = IntList
1304
+ // LegacyData has two annotations
1305
+ TypeId.of[LegacyData].annotations.map(_.name)
1306
+ // res153: List[String] = List("transient", "deprecated")
856
1307
 
857
- // Normalizing MyIntList resolves through IntList to List[Int]
1308
+ // Plain has no annotations
1309
+ TypeId.of[Plain].annotations
1310
+ // res154: List[Annotation] = List()
858
1311
  ```
859
1312
 
860
- ## Equality and Hashing
1313
+ #### `selfType` — Self-Type Annotation
861
1314
 
862
- TypeId uses structural equality that accounts for type aliases:
1315
+ Returns `Some(typeRepr)` when the trait declares a self-type (e.g., `trait Foo { self: Bar => ... }`), and `None` otherwise. Self-types express a dependency requirement: a trait that declares `self: Logger =>` can only be mixed into a class that also mixes in `Logger`. This method lets frameworks detect and validate those requirements at runtime:
863
1316
 
864
1317
  ```scala
865
- val alias1 = TypeId.alias[A]("A", owner, Nil, TypeRepr.Ref(TypeId.int))
866
- val alias2 = TypeId.alias[A]("A", owner, Nil, TypeRepr.Ref(TypeId.int))
1318
+ sealed trait TypeId[A <: AnyKind] {
1319
+ def selfType: Option[TypeRepr]
1320
+ }
1321
+ ```
867
1322
 
868
- alias1 == alias2 // true (structural equality)
1323
+ ```scala
1324
+ import zio.blocks.typeid._
869
1325
 
870
- // Works correctly in hash maps
871
- val map = Map(alias1 -> "value")
872
- map(alias2) // "value"
1326
+ trait Logger { def log(msg: String): Unit }
1327
+ trait Service { self: Logger => def doWork(): Unit = log("working") }
873
1328
  ```
874
1329
 
875
- ## Erased TypeId
876
-
877
- For type-indexed collections where the type parameter doesn't matter:
1330
+ `Service` requires a `Logger` to be mixed in, while `Logger` has no self-type requirement:
878
1331
 
879
1332
  ```scala
880
- // TypeId.Erased is TypeId[TypeId.Unknown]
881
- val erased: TypeId.Erased = typeId.erased
1333
+ // Service declares a self-type dependency on Logger
1334
+ TypeId.of[Service].selfType
1335
+ // res156: Option[TypeRepr] = Some(Logger & Service)
882
1336
 
883
- // Use in maps keyed by type
884
- val typeRegistry: Map[TypeId.Erased, Schema[_]] = Map(
885
- TypeId.int.erased -> Schema[Int],
886
- TypeId.string.erased -> Schema[String]
887
- )
1337
+ // Logger has no self-type requirement
1338
+ TypeId.of[Logger].selfType
1339
+ // res157: Option[TypeRepr] = None
888
1340
  ```
889
1341
 
890
- ## Runtime Reflection
1342
+ #### `aliasedTo` — Alias Target
1343
+
1344
+ Returns `Some(typeRepr)` for type aliases pointing to their underlying type, and `None` for nominal and opaque types. This lets you inspect what a type alias expands to without evaluating expressions at runtime:
891
1345
 
892
- On JVM, TypeId can retrieve the corresponding `Class`:
1346
+ ```scala
1347
+ sealed trait TypeId[A <: AnyKind] {
1348
+ def aliasedTo: Option[TypeRepr]
1349
+ }
1350
+ ```
893
1351
 
894
1352
  ```scala
895
- val typeId = TypeId.of[Person]
896
- val clazz: Option[Class[_]] = typeId.clazz
1353
+ import zio.blocks.typeid._
1354
+
1355
+ type Age = Int
1356
+ type Name = String
1357
+ ```
1358
+
1359
+ A type alias resolves to its target; a concrete type returns `None`:
1360
+
1361
+ ```scala
1362
+ // Age is an alias for Int
1363
+ TypeId.of[Age].aliasedTo
1364
+ // res159: Option[TypeRepr] = Some(Ref(Int))
1365
+
1366
+ // Name is an alias for String
1367
+ TypeId.of[Name].aliasedTo
1368
+ // res160: Option[TypeRepr] = Some(Ref(String))
1369
+
1370
+ // Int is a concrete type, not an alias
1371
+ TypeId.of[Int].aliasedTo
1372
+ // res161: Option[TypeRepr] = None
1373
+ ```
1374
+
1375
+ #### `representation` — Opaque Type Representation
1376
+
1377
+ Returns `Some(typeRepr)` for opaque types revealing their underlying representation type, and `None` for all other types. Opaque types hide their implementation behind a new name, but `representation` lets frameworks such as serializers discover what the type is actually stored as:
1378
+
1379
+ ```scala
1380
+ sealed trait TypeId[A <: AnyKind] {
1381
+ def representation: Option[TypeRepr]
1382
+ }
1383
+ ```
1384
+
1385
+ ```scala
1386
+ import zio.blocks.typeid._
1387
+
1388
+ opaque type Email = String
1389
+ opaque type UserId = Int
1390
+ ```
1391
+
1392
+ An opaque type exposes its representation; a non-opaque type returns `None`:
1393
+
1394
+ ```scala
1395
+ // Email is an opaque type backed by String
1396
+ TypeId.of[Email].representation
1397
+ // res163: Option[TypeRepr] = Some(Ref(String))
1398
+
1399
+ // UserId is an opaque type backed by Int
1400
+ TypeId.of[UserId].representation
1401
+ // res164: Option[TypeRepr] = Some(Ref(Int))
1402
+
1403
+ // Int is not opaque
1404
+ TypeId.of[Int].representation
1405
+ // res165: Option[TypeRepr] = None
1406
+ ```
1407
+
1408
+ ### Erasure and Runtime
1409
+
1410
+ Methods for type erasure, runtime class lookup, and reflective construction.
1411
+
1412
+ #### `erased` — Erase Type Parameter
1413
+
1414
+ Erases the phantom type parameter, returning a `TypeId.Erased` (alias for `TypeId[TypeId.Unknown]`). This is useful when you need to store heterogeneous `TypeId` values in a collection or a type-indexed map, where the exact type parameter is unknown or irrelevant at the storage site:
1415
+
1416
+ ```scala
1417
+ sealed trait TypeId[A <: AnyKind] {
1418
+ def erased: TypeId.Erased
1419
+ }
1420
+ ```
1421
+
1422
+ Different types can be stored together once erased:
1423
+
1424
+ ```scala
1425
+ val ids: List[TypeId.Erased] = List(
1426
+ TypeId.of[Int].erased,
1427
+ TypeId.of[String].erased,
1428
+ TypeId.of[Boolean].erased
1429
+ )
1430
+ // ids: List[Erased] = List(Int, String, Boolean)
1431
+
1432
+ ids.map(_.name)
1433
+ // res166: List[String] = List("Int", "String", "Boolean")
1434
+ ```
1435
+
1436
+ #### `classTag` — Runtime ClassTag
1437
+
1438
+ Returns a `ClassTag` for this type. Returns the correct primitive `ClassTag` for Scala primitive types (`Int`, `Long`, `Boolean`, etc.) and `ClassTag.AnyRef` for all reference types. This is useful when you need to create properly-typed arrays or work with generic collections that require implicit `ClassTag` evidence at runtime:
1439
+
1440
+ ```scala
1441
+ sealed trait TypeId[A <: AnyKind] {
1442
+ lazy val classTag: scala.reflect.ClassTag[?]
1443
+ }
1444
+ ```
1445
+
1446
+ On the JVM, arrays are reified — the element type is part of the array object at runtime, not erased like generics. To create an array of a generic type `T`, Scala requires a `ClassTag[T]` so the runtime knows whether to allocate a primitive array (`int[]`, `double[]`) or an object array (`Object[]`). This matters for memory efficiency: a primitive `int[]` stores 4 bytes per element unboxed, while an `Integer[]` stores heap references plus the cost of boxing each value.
1447
+
1448
+ `classTag` returns the correct `ClassTag` for each type:
1449
+
1450
+ ```scala
1451
+ // Primitive types have dedicated ClassTags
1452
+ TypeId.of[Int].classTag
1453
+ // res167: ClassTag[_ >: Nothing <: Any] = Int
1454
+ TypeId.of[Double].classTag
1455
+ // res168: ClassTag[_ >: Nothing <: Any] = Double
1456
+
1457
+ // Reference types use ClassTag.AnyRef
1458
+ TypeId.of[String].classTag
1459
+ // res169: ClassTag[_ >: Nothing <: Any] = Object
1460
+ TypeId.of[List[Int]].classTag
1461
+ // res170: ClassTag[_ >: Nothing <: Any] = Object
1462
+ ```
1463
+
1464
+ A concrete use case is a generic storage allocator that creates the right array type from a `TypeId`:
1465
+
1466
+ ```scala
1467
+ import zio.blocks.typeid._
1468
+
1469
+ def makeStorage(size: Int, id: TypeId[?]): Array[?] =
1470
+ id.classTag.newArray(size)
1471
+ ```
1472
+
1473
+ ```scala
1474
+ // Creates int[] (primitive, unboxed)
1475
+ makeStorage(100, TypeId.int).getClass.getComponentType
1476
+ // res172: Class[_ >: Nothing <: <FromJavaObject>] = int
1477
+
1478
+ // Creates double[] (primitive, unboxed)
1479
+ makeStorage(100, TypeId.double).getClass.getComponentType
1480
+ // res173: Class[_ >: Nothing <: <FromJavaObject>] = double
1481
+
1482
+ // Creates Object[] (reference)
1483
+ makeStorage(100, TypeId.string).getClass.getComponentType
1484
+ // res174: Class[_ >: Nothing <: <FromJavaObject>] = class java.lang.Object
1485
+ ```
1486
+
1487
+ Another use case is detecting primitive types. Without `classTag`, you would need to enumerate every primitive with a chain of `isInstanceOf` checks:
1488
+
1489
+ ```scala
1490
+ // Without classTag: every primitive listed explicitly
1491
+ def isPrimitive(value: Any): Boolean =
1492
+ value.isInstanceOf[Int] ||
1493
+ value.isInstanceOf[Long] ||
1494
+ value.isInstanceOf[Float] ||
1495
+ value.isInstanceOf[Double] ||
1496
+ value.isInstanceOf[Boolean] ||
1497
+ value.isInstanceOf[Byte] ||
1498
+ value.isInstanceOf[Short] ||
1499
+ value.isInstanceOf[Char]
1500
+ ```
1501
+
1502
+ This is fragile: if you forget one primitive (e.g. `Unit`) the check silently breaks. With `classTag` the same question reduces to a single comparison that can never miss a case — `ClassTag.AnyRef` is the universal fallback for every reference type, so anything that is not `AnyRef` must be a primitive:
1503
+
1504
+ ```scala
1505
+ import zio.blocks.typeid._
1506
+
1507
+ def isPrimitive(id: TypeId[?]): Boolean =
1508
+ id.classTag != scala.reflect.ClassTag.AnyRef
1509
+ ```
1510
+
1511
+ ```scala
1512
+ isPrimitive(TypeId.of[Int])
1513
+ // res176: Boolean = true
1514
+ isPrimitive(TypeId.of[Double])
1515
+ // res177: Boolean = true
1516
+ isPrimitive(TypeId.of[Boolean])
1517
+ // res178: Boolean = true
1518
+ isPrimitive(TypeId.of[String])
1519
+ // res179: Boolean = false
1520
+ isPrimitive(TypeId.of[List[Int]])
1521
+ // res180: Boolean = false
1522
+ ```
1523
+
1524
+ :::note
1525
+ `classTag` returns `ClassTag.AnyRef` for all reference types. For matching or filtering by a specific reference type at runtime, use `clazz` instead.
1526
+ :::
1527
+
1528
+ #### `clazz` — Runtime Class
1529
+
1530
+ Returns the runtime `Class[_]` for this type. On the JVM it returns `Some(Class[_])` for nominal and applied types, and `None` for alias and opaque types. On Scala.js it always returns `None` since JVM reflection is unavailable. This is the entry point for reflective operations such as instantiation, field access, or integration with Java libraries:
1531
+
1532
+ ```scala
1533
+ sealed trait TypeId[A <: AnyKind] {
1534
+ def clazz: Option[Class[?]]
1535
+ }
1536
+ ```
1537
+
1538
+ ```scala
1539
+ import zio.blocks.typeid._
1540
+
1541
+ type Age = Int
1542
+ ```
1543
+
1544
+ ```scala
1545
+ // Nominal and applied types return Some on the JVM
1546
+ TypeId.of[String].clazz
1547
+ // res182: Option[Class[_ >: Nothing <: Any]] = Some(class java.lang.String)
1548
+ TypeId.of[Int].clazz
1549
+ // res183: Option[Class[_ >: Nothing <: Any]] = Some(int)
1550
+ TypeId.of[List[Int]].clazz
1551
+ // res184: Option[Class[_ >: Nothing <: Any]] = Some(
1552
+ // class scala.collection.immutable.List
1553
+ // )
1554
+
1555
+ // Alias types return None — the alias has no class of its own
1556
+ TypeId.of[Age].clazz
1557
+ // res185: Option[Class[_ >: Nothing <: Any]] = None
1558
+ ```
1559
+
1560
+ :::note
1561
+ On Scala.js, `clazz` always returns `None`. Use `classTag` instead when you need cross-platform runtime type information.
1562
+ :::
1563
+
1564
+ #### `construct` — Reflective Construction
1565
+
1566
+ Constructs an instance using the primary constructor on the JVM by passing constructor arguments as a `Chunk[AnyRef]`. Returns `Left` with an error message on Scala.js or when construction fails (wrong argument count, wrong types, or abstract types). Primitive values must be explicitly boxed since the argument type is `AnyRef`:
1567
+
1568
+ ```scala
1569
+ sealed trait TypeId[A <: AnyKind] {
1570
+ def construct(args: Chunk[AnyRef]): Either[String, Any]
1571
+ }
1572
+ ```
1573
+
1574
+ ```scala
1575
+ import zio.blocks.typeid._
1576
+ import zio.blocks.chunk.Chunk
1577
+
1578
+ // JVM only
1579
+ case class User(name: String, age: Int)
1580
+
1581
+ val userId = TypeId.of[User]
1582
+ ```
1583
+
1584
+ ```scala
1585
+ userId.construct(Chunk("Alice", 30: Integer))
1586
+ // res187: Either[String, Any] = Left(
1587
+ // "Cannot construct repl.MdocSession.MdocApp186.User: class not available"
1588
+ // )
1589
+ userId.construct(Chunk("Bob"))
1590
+ // res188: Either[String, Any] = Left(
1591
+ // "Cannot construct repl.MdocSession.MdocApp186.User: class not available"
1592
+ // )
1593
+ ```
1594
+
1595
+ ##### Collection Types
1596
+
1597
+ Collection types accept variadic arguments representing elements. Sequence-like types (`List`, `Vector`, `Set`, `Seq`, `IndexedSeq`, `Array`, `ArraySeq`, `Chunk`) each pass a variadic sequence of elements:
1598
+
1599
+ ```scala
1600
+ import zio.blocks.typeid._
1601
+ import zio.blocks.chunk.Chunk
1602
+ ```
1603
+
1604
+ ```scala
1605
+ TypeId.of[List[String]].construct(Chunk("a", "b", "c"))
1606
+ // res190: Either[String, Any] = Right(List("a", "b", "c"))
1607
+ TypeId.of[Vector[Int]].construct(Chunk(1: Integer, 2: Integer, 3: Integer))
1608
+ // res191: Either[String, Any] = Right(Vector(1, 2, 3))
1609
+ TypeId.of[Set[String]].construct(Chunk("x", "y", "z"))
1610
+ // res192: Either[String, Any] = Right(Set("x", "y", "z"))
1611
+ ```
1612
+
1613
+ Map types pass interleaved key-value pairs and fail on odd argument counts:
1614
+
1615
+ ```scala
1616
+ TypeId.of[Map[String, Int]].construct(Chunk("a", 1: Integer, "b", 2: Integer))
1617
+ // res193: Either[String, Any] = Right(Map("a" -> 1, "b" -> 2))
1618
+ ```
1619
+
1620
+ ##### Sum Types
1621
+
1622
+ Sum types have special calling conventions. `Option` accepts 1 element to construct `Some(value)`, or 0 elements to construct `None`:
1623
+
1624
+ ```scala
1625
+ TypeId.of[Option[String]].construct(Chunk("hello"))
1626
+ // res194: Either[String, Any] = Right(Some("hello"))
1627
+ TypeId.of[Option[String]].construct(Chunk())
1628
+ // res195: Either[String, Any] = Right(None)
1629
+ ```
1630
+
1631
+ `Either` requires a Boolean flag as the first argument (`true` for `Right`, `false` for `Left`), followed by the value:
1632
+
1633
+ ```scala
1634
+ TypeId.of[Either[String, Int]].construct(Chunk(true: java.lang.Boolean, 42: Integer))
1635
+ // res196: Either[String, Any] = Right(Right(42))
1636
+ TypeId.of[Either[String, Int]].construct(Chunk(false: java.lang.Boolean, "error"))
1637
+ // res197: Either[String, Any] = Right(Left("error"))
1638
+ ```
1639
+
1640
+ ### Normalization and Equality
1641
+
1642
+ **Normalization** resolves type aliases and opaque type representations to their underlying concrete types. For example, `type Age = Int` normalizes to `Int`, and chained aliases like `type UserId = NonEmpty; type NonEmpty = List[Int]` both resolve to `List[Int]`. Opaque types such as `opaque type UserId = String` normalize to their representation. Normalization is crucial because multiple syntactic names often refer to the same underlying type, enabling deduplication and caching strategies.
1643
+
1644
+ **Structural Equality** compares two TypeIds by their normalized form, treating types with identical underlying structure as equal. Importantly, opaque types preserve their semantic identity even after normalization—`TypeId.of[UserId]` where `opaque type UserId = String` remains distinct from `TypeId.of[String]` for equality purposes, preserving runtime type safety. This distinction enables type-safe registries and validators that respect opaque type boundaries.
1645
+
1646
+ These concepts are essential for building type-indexed registries that recognize multiple alias names as referring to the same handler, implementing serialization strategies based on normalized type structure, and enforcing opaque type safety in type-indexed maps where different opaque types wrapping the same base type should have separate validators or handlers.
1647
+
1648
+ #### `TypeId.normalize` — Resolve Aliases
1649
+
1650
+ Resolves chains of type aliases to the underlying type. For example, `type MyList = List[Int]` normalizes to `List[Int]`:
1651
+
1652
+ ```scala
1653
+ object TypeId {
1654
+ def normalize(id: TypeId[?]): TypeId[?]
1655
+ }
1656
+ ```
1657
+
1658
+ ```scala
1659
+ import zio.blocks.typeid._
1660
+
1661
+ type Age = Int
1662
+ ```
1663
+
1664
+ ```scala
1665
+ val ageId = TypeId.of[Age]
1666
+ // ageId: TypeId[Age] = Age
1667
+ val norm = TypeId.normalize(ageId)
1668
+ // norm: TypeId[_ >: Nothing <: AnyKind] = Int
1669
+ norm.fullName
1670
+ // res199: String = "scala.Int"
1671
+ ```
1672
+
1673
+ #### `TypeId.structurallyEqual` — Structural Equality
1674
+
1675
+ Checks if two TypeIds are structurally equal after normalization. Semantically equivalent to `==` on TypeId instances; `==` additionally short-circuits on hash mismatch for performance:
1676
+
1677
+ ```scala
1678
+ object TypeId {
1679
+ def structurallyEqual(a: TypeId[?], b: TypeId[?]): Boolean
1680
+ }
1681
+ ```
1682
+
1683
+ ```scala
1684
+ import zio.blocks.typeid._
1685
+
1686
+ type UserId = Int
1687
+ ```
1688
+
1689
+ ```scala
1690
+ val a = TypeId.of[UserId]
1691
+ // a: TypeId[UserId] = UserId
1692
+ val b = TypeId.of[Int]
1693
+ // b: TypeId[Int] = Int
1694
+ TypeId.structurallyEqual(a, b)
1695
+ // res201: Boolean = true
1696
+ a == b
1697
+ // res202: Boolean = true
1698
+ ```
1699
+
1700
+ #### `TypeId.structuralHash` — Structural Hash Code
1701
+
1702
+ Computes a hash code based on the normalized structural representation:
1703
+
1704
+ ```scala
1705
+ object TypeId {
1706
+ def structuralHash(id: TypeId[?]): Int
1707
+ }
1708
+ ```
1709
+
1710
+ #### `TypeId.unapplied` — Strip Type Arguments
1711
+
1712
+ Returns the type constructor by stripping all type arguments. For example, `TypeId.unapplied(TypeId.of[List[Int]])` returns the equivalent of `TypeId.of[List]`:
1713
+
1714
+ ```scala
1715
+ object TypeId {
1716
+ def unapplied(id: TypeId[?]): TypeId[?]
1717
+ }
1718
+ ```
1719
+
1720
+ ```scala
1721
+ val listInt = TypeId.of[List[Int]]
1722
+ // listInt: TypeId[List[Int]] = List[Int]
1723
+ val unapplied = TypeId.unapplied(listInt)
1724
+ // unapplied: TypeId[_ >: Nothing <: AnyKind] = List[+A]
1725
+ unapplied.isApplied
1726
+ // res203: Boolean = false
1727
+ unapplied.name
1728
+ // res204: String = "List"
1729
+ ```
1730
+
1731
+ ### Pattern Matching Extractors
1732
+
1733
+ The companion object provides extractors for pattern matching on TypeId classification:
1734
+
1735
+ ```scala
1736
+ import zio.blocks.typeid._
1737
+
1738
+ case class User(id: Long, email: String)
1739
+ val userId = TypeId.of[User]
1740
+ ```
1741
+
1742
+ ```scala
1743
+ userId match {
1744
+ case TypeId.Nominal(name, owner, params, defKind, parents) =>
1745
+ s"Nominal type '$name' in ${owner.asString}"
1746
+ case TypeId.Alias(name, _, _, aliased) =>
1747
+ s"Alias '$name'"
1748
+ case TypeId.Opaque(name, _, _, repr, _) =>
1749
+ s"Opaque '$name'"
1750
+ }
1751
+ // res206: String = "Nominal type 'User' in repl.MdocSession.MdocApp205"
1752
+ ```
1753
+
1754
+ The extractors are:
1755
+
1756
+ | Extractor | Matches |
1757
+ |---------------------------------------------------------|--------------------------|
1758
+ | `TypeId.Nominal(name, owner, params, defKind, parents)` | Classes, traits, objects |
1759
+ | `TypeId.Alias(name, owner, params, aliased)` | Type aliases |
1760
+ | `TypeId.Opaque(name, owner, params, repr, bounds)` | Opaque types |
1761
+ | `TypeId.Sealed(name)` | Sealed traits |
1762
+ | `TypeId.Enum(name, owner)` | Scala 3 enums |
1763
+
1764
+ ## TypeDefKind Reference
1765
+
1766
+ `TypeDefKind` classifies every type definition. Access it via the `defKind` property documented in [Core Operations](#type-classification).
1767
+
1768
+ The `defKind` property (documented in [Core Operations](#type-classification)) returns one of these variants. Use classification predicates like `isCaseClass`, `isSealed`, `isObject` for simple checks.
1769
+
1770
+ `TypeDefKind` has these variants:
1771
+
1772
+ | Variant | Description |
1773
+ |------------------------------------------------------|----------------------------------------------|
1774
+ | `Class(isFinal, isAbstract, isCase, isValue, bases)` | Class definitions |
1775
+ | `Trait(isSealed, bases)` | Trait definitions |
1776
+ | `Object(bases)` | Singleton objects |
1777
+ | `Enum(bases)` | Scala 3 enums |
1778
+ | `EnumCase(parentEnum, ordinal, isObjectCase)` | Enum cases |
1779
+ | `TypeAlias` | Type aliases (`type Foo = Bar`) |
1780
+ | `OpaqueType(publicBounds)` | Opaque types |
1781
+ | `AbstractType` | Abstract type members |
1782
+ | `Unknown` | Unclassified or unresolvable type definition |
1783
+
1784
+ ## Type Parameters and Generics
1785
+
1786
+ When you derive a TypeId for a generic type, the macro captures its type parameters (variance, bounds, kind) and any applied type arguments.
1787
+
1788
+ A **raw type constructor** is a generic type without any type arguments filled in. For example, `List` by itself (without `[Int]` or `[String]`) is a raw type constructor. Scala 3 supports deriving TypeIds directly for raw type constructors, but Scala 2 has restrictions due to its type system:
1789
+
1790
+ **Scala 3** allows you to work with raw type constructors directly:
1791
+
1792
+ ```scala
1793
+ // Scala 3 only
1794
+ val listId = TypeId.of[List] // Works: raw type constructor
1795
+ ```
1796
+
1797
+ **Scala 2** requires you to use a wildcard placeholder or implicit derivation since raw type constructors are not valid syntax:
1798
+
1799
+ ```scala
1800
+ // Scala 2 alternatives
1801
+ val listId = TypeId.of[List[_]] // Use wildcard type argument
1802
+
1803
+ // Or retrieve via implicit derivation
1804
+ implicit val derived: TypeId[List[_]] = TypeId.of[List[_]]
1805
+ ```
1806
+
1807
+ This distinction matters when you need to capture the type constructor itself (for higher-kinded type operations) rather than concrete types like `List[Int]`.
1808
+
1809
+ ### Inspecting Type Parameters
1810
+
1811
+ Define your own generic types and derive their TypeIds to see how type parameters are captured:
1812
+
1813
+ ```scala
1814
+ import zio.blocks.typeid._
1815
+
1816
+ sealed trait Container[+A]
1817
+ case class Box[+A](value: A) extends Container[A]
1818
+
1819
+ sealed trait Cache[K, +V]
1820
+ case class LRUCache[K, +V](maxSize: Int) extends Cache[K, V]
1821
+ ```
1822
+
1823
+ A single-parameter type constructor:
1824
+
1825
+ ```scala
1826
+ val containerId = TypeId.of[Container]
1827
+ // containerId: TypeId[[A >: Nothing <: Any] =>> Container[A]] = Container[+A]
1828
+ containerId.typeParams
1829
+ // res208: List[TypeParam] = List(
1830
+ // TypeParam(
1831
+ // name = "A",
1832
+ // index = 0,
1833
+ // variance = Covariant,
1834
+ // bounds = TypeBounds(lower = None, upper = None),
1835
+ // kind = Type
1836
+ // )
1837
+ // )
1838
+ containerId.arity
1839
+ // res209: Int = 1
1840
+ ```
1841
+
1842
+ A two-parameter type constructor with mixed variance (invariant `K`, covariant `V`):
1843
+
1844
+ ```scala
1845
+ val cacheId = TypeId.of[Cache]
1846
+ // cacheId: TypeId[[K >: Nothing <: Any, V >: Nothing <: Any] =>> Cache[K, V]] = Cache[K, +V]
1847
+ cacheId.typeParams
1848
+ // res210: List[TypeParam] = List(
1849
+ // TypeParam(
1850
+ // name = "K",
1851
+ // index = 0,
1852
+ // variance = Invariant,
1853
+ // bounds = TypeBounds(lower = None, upper = None),
1854
+ // kind = Type
1855
+ // ),
1856
+ // TypeParam(
1857
+ // name = "V",
1858
+ // index = 1,
1859
+ // variance = Covariant,
1860
+ // bounds = TypeBounds(lower = None, upper = None),
1861
+ // kind = Type
1862
+ // )
1863
+ // )
1864
+ cacheId.arity
1865
+ // res211: Int = 2
1866
+ ```
1867
+
1868
+ Each `TypeParam` records the parameter's name, position, variance, bounds, and kind:
1869
+
1870
+ ```scala
1871
+ val containerParam = containerId.typeParams.head
1872
+ // containerParam: TypeParam = TypeParam(
1873
+ // name = "A",
1874
+ // index = 0,
1875
+ // variance = Covariant,
1876
+ // bounds = TypeBounds(lower = None, upper = None),
1877
+ // kind = Type
1878
+ // )
1879
+ containerParam.name
1880
+ // res212: String = "A"
1881
+ containerParam.variance
1882
+ // res213: Variance = Covariant
1883
+ containerParam.kind
1884
+ // res214: Kind = Type
1885
+ containerParam.isCovariant
1886
+ // res215: Boolean = true
1887
+ ```
1888
+
1889
+ ```scala
1890
+ val cacheParams = cacheId.typeParams
1891
+ // cacheParams: List[TypeParam] = List(
1892
+ // TypeParam(
1893
+ // name = "K",
1894
+ // index = 0,
1895
+ // variance = Invariant,
1896
+ // bounds = TypeBounds(lower = None, upper = None),
1897
+ // kind = Type
1898
+ // ),
1899
+ // TypeParam(
1900
+ // name = "V",
1901
+ // index = 1,
1902
+ // variance = Covariant,
1903
+ // bounds = TypeBounds(lower = None, upper = None),
1904
+ // kind = Type
1905
+ // )
1906
+ // )
1907
+ cacheParams.map(p => (p.name, p.variance))
1908
+ // res216: List[Tuple2[String, Variance]] = List(
1909
+ // ("K", Invariant),
1910
+ // ("V", Covariant)
1911
+ // )
1912
+ ```
1913
+
1914
+ ### Inspecting Type Arguments
1915
+
1916
+ When you derive a TypeId for an *applied* type (a generic type with concrete arguments), the type arguments are captured:
1917
+
1918
+ ```scala
1919
+ val boxIntId = TypeId.of[Box[Int]]
1920
+ // boxIntId: TypeId[Box[Int]] = Box[Int]
1921
+ boxIntId.typeArgs
1922
+ // res217: List[TypeRepr] = List(Ref(Int))
1923
+ boxIntId.isApplied
1924
+ // res218: Boolean = true
1925
+ ```
1926
+
1927
+ ```scala
1928
+ val cacheStringIntId = TypeId.of[LRUCache[String, Int]]
1929
+ // cacheStringIntId: TypeId[LRUCache[String, Int]] = LRUCache[String, Int]
1930
+ cacheStringIntId.typeArgs
1931
+ // res219: List[TypeRepr] = List(Ref(String), Ref(Int))
1932
+ ```
1933
+
1934
+ ### Variance
1935
+
1936
+ **Variance** describes how a type parameter's subtyping relationships are preserved. **Covariant** types (`+`) preserve subtyping (if `B <: A` then `Container[B] <: Container[A]`), **contravariant** types (`-`) reverse it, and **invariant** types preserve neither. For example, `Container[+A]` is covariant—a `Container[String]` can be used where `Container[Any]` is expected. In contrast, `Cache[K, V]` where `K` is invariant means `Cache[String, Int]` cannot substitute for `Cache[Any, Int]` even if `String <: Any`.
1937
+
1938
+ Variance matters for type safety, polymorphism, and API design. TypeId captures variance information, enabling runtime inspection and validation:
1939
+
1940
+ ```scala
1941
+ import zio.blocks.typeid._
1942
+
1943
+ sealed trait Container[+A]
1944
+ sealed trait Cache[K, +V]
1945
+ ```
1946
+
1947
+ ```scala
1948
+ val containerParams = TypeId.of[Container].typeParams
1949
+ // containerParams: List[TypeParam] = List(
1950
+ // TypeParam(
1951
+ // name = "A",
1952
+ // index = 0,
1953
+ // variance = Covariant,
1954
+ // bounds = TypeBounds(lower = None, upper = None),
1955
+ // kind = Type
1956
+ // )
1957
+ // )
1958
+ containerParams.map(p => (p.name, p.variance))
1959
+ // res221: List[Tuple2[String, Variance]] = List(("A", Covariant))
1960
+
1961
+ val cacheParams = TypeId.of[Cache].typeParams
1962
+ // cacheParams: List[TypeParam] = List(
1963
+ // TypeParam(
1964
+ // name = "K",
1965
+ // index = 0,
1966
+ // variance = Invariant,
1967
+ // bounds = TypeBounds(lower = None, upper = None),
1968
+ // kind = Type
1969
+ // ),
1970
+ // TypeParam(
1971
+ // name = "V",
1972
+ // index = 1,
1973
+ // variance = Covariant,
1974
+ // bounds = TypeBounds(lower = None, upper = None),
1975
+ // kind = Type
1976
+ // )
1977
+ // )
1978
+ cacheParams.map(p => (p.name, p.variance))
1979
+ // res222: List[Tuple2[String, Variance]] = List(
1980
+ // ("K", Invariant),
1981
+ // ("V", Covariant)
1982
+ // )
1983
+ ```
1984
+
1985
+ You can also work with variance values directly:
1986
+
1987
+ ```scala
1988
+ Variance.Covariant.symbol
1989
+ // res223: String = "+"
1990
+ Variance.Contravariant.symbol
1991
+ // res224: String = "-"
1992
+ Variance.Invariant.symbol
1993
+ // res225: String = ""
1994
+ Variance.Covariant.flip
1995
+ // res226: Variance = Contravariant
1996
+ ```
1997
+
1998
+ ### Kind
1999
+
2000
+ **Kind** describes the "type of a type"—it captures whether something is a concrete type or a type constructor, and how many type parameters it requires. A **proper type** like `Int` or `Box[String]` has kind `*` (zero parameters). A **unary type constructor** like `List` has kind `* -> *` (takes one type parameter). A **binary type constructor** like `Map` has kind `* -> * -> *` (takes two). **Higher-kinded types** like `Runnable[F[_]]` have kinds like `(* -> *) -> *` (takes a type constructor as a parameter). Kind information is essential for generic programming, enforcing API contracts, and enabling advanced patterns like monad transformers.
2001
+
2002
+ TypeId captures kind information at runtime, allowing you to inspect and validate the structure of types:
2003
+
2004
+ ```scala
2005
+ import zio.blocks.typeid._
2006
+
2007
+ sealed trait Container[+A]
2008
+ case class Box[+A](value: A) extends Container[A]
2009
+
2010
+ sealed trait Cache[K, +V]
2011
+ case class LRUCache[K, +V](maxSize: Int) extends Cache[K, V]
2012
+
2013
+ trait Runnable[F[_]] {
2014
+ def run[A](fa: F[A]): A
2015
+ }
2016
+ ```
2017
+
2018
+ A proper type (`*`) — fully concrete with no type parameters:
2019
+
2020
+ ```scala
2021
+ val boxIntId = TypeId.of[Box[Int]]
2022
+ // boxIntId: TypeId[Box[Int]] = Box[Int]
2023
+ boxIntId.isApplied
2024
+ // res228: Boolean = true
2025
+ boxIntId.arity
2026
+ // res229: Int = 1
2027
+ ```
2028
+
2029
+ A unary type constructor (`* -> *`) — takes one type parameter:
2030
+
2031
+ ```scala
2032
+ val containerId = TypeId.of[Container]
2033
+ // containerId: TypeId[[A >: Nothing <: Any] =>> Container[A]] = Container[+A]
2034
+ containerId.arity
2035
+ // res230: Int = 1
2036
+ containerId.typeParams.map(p => (p.name, p.kind))
2037
+ // res231: List[Tuple2[String, Kind]] = List(("A", Type))
2038
+ ```
2039
+
2040
+ A binary type constructor (`* -> * -> *`) — takes two type parameters:
2041
+
2042
+ ```scala
2043
+ val cacheId = TypeId.of[Cache]
2044
+ // cacheId: TypeId[[K >: Nothing <: Any, V >: Nothing <: Any] =>> Cache[K, V]] = Cache[K, +V]
2045
+ cacheId.arity
2046
+ // res232: Int = 2
2047
+ cacheId.typeParams.map(p => (p.name, p.kind))
2048
+ // res233: List[Tuple2[String, Kind]] = List(("K", Type), ("V", Type))
2049
+ ```
2050
+
2051
+ A higher-kinded type (`(* -> *) -> *`) — a type parameter that itself is a type constructor:
2052
+
2053
+ ```scala
2054
+ val runnableId = TypeId.of[Runnable]
2055
+ // runnableId: TypeId[[F >: Nothing <: [_$4 >: Nothing <: Any] =>> Any] =>> Runnable[F]] = Runnable[F]
2056
+ runnableId.typeParams.head.kind
2057
+ // res234: Kind = Type
2058
+ runnableId.typeParams.head.kind.arity
2059
+ // res235: Int = 0
2060
+ ```
2061
+
2062
+ | Kind | Notation | Arity | Examples |
2063
+ |---------------------------|-----------------|-------|-----------------------|
2064
+ | `Kind.Type` / `Kind.Star` | `*` | 0 | `Int`, `Box[Int]` |
2065
+ | `Kind.Star1` | `* -> *` | 1 | `Container`, `Option` |
2066
+ | `Kind.Star2` | `* -> * -> *` | 2 | `Cache`, `Either` |
2067
+ | `Kind.HigherStar1` | `(* -> *) -> *` | 1 | `Runnable` |
2068
+
2069
+ ## Subtype Relationships
2070
+
2071
+ **Subtype relationships** determine if one type is a subtype of another, enabling type-safe polymorphism and dispatch at runtime. This is essential for checking if a value of one type can be used where another type is expected. TypeId handles direct inheritance, transitive inheritance chains, sealed trait cases, and variance-aware subtyping for generic types.
2072
+
2073
+ Three key methods work together to express the full range of type relationships:
2074
+
2075
+ ```scala
2076
+ import zio.blocks.typeid._
2077
+
2078
+ sealed trait Animal
2079
+ sealed trait Mammal extends Animal
2080
+ case class Dog(name: String) extends Mammal
2081
+ case class Cat(name: String) extends Mammal
2082
+ case class Fish(species: String) extends Animal
2083
+ ```
2084
+
2085
+ **`isSubtypeOf`** checks if this type is a subtype of another (direct or transitive):
2086
+
2087
+ ```scala
2088
+ val dogId = TypeId.of[Dog]
2089
+ // dogId: TypeId[Dog] = Dog
2090
+ val mammalId = TypeId.of[Mammal]
2091
+ // mammalId: TypeId[Mammal] = Mammal
2092
+ val animalId = TypeId.of[Animal]
2093
+ // animalId: TypeId[Animal] = Animal
2094
+ val fishId = TypeId.of[Fish]
2095
+ // fishId: TypeId[Fish] = Fish
2096
+
2097
+ // Direct inheritance: Dog extends Mammal
2098
+ dogId.isSubtypeOf(mammalId)
2099
+ // res237: Boolean = true
2100
+
2101
+ // Transitive inheritance: Dog extends Mammal extends Animal
2102
+ dogId.isSubtypeOf(animalId)
2103
+ // res238: Boolean = true
2104
+
2105
+ // Not a subtype relationship
2106
+ dogId.isSubtypeOf(fishId)
2107
+ // res239: Boolean = false
2108
+ fishId.isSubtypeOf(mammalId)
2109
+ // res240: Boolean = false
2110
+ ```
2111
+
2112
+ **`isSupertypeOf`** is the reverse—checks if this type is a supertype (parent) of another:
2113
+
2114
+ ```scala
2115
+ mammalId.isSupertypeOf(dogId)
2116
+ // res241: Boolean = true
2117
+ animalId.isSupertypeOf(dogId)
2118
+ // res242: Boolean = true
2119
+ dogId.isSupertypeOf(animalId)
2120
+ // res243: Boolean = false
2121
+ ```
2122
+
2123
+ **`isEquivalentTo`** checks if two types are exactly the same:
2124
+
2125
+ ```scala
2126
+ dogId.isEquivalentTo(dogId)
2127
+ // res244: Boolean = true
2128
+ dogId.isEquivalentTo(mammalId)
2129
+ // res245: Boolean = false
2130
+ animalId.isEquivalentTo(animalId)
2131
+ // res246: Boolean = true
2132
+ ```
2133
+
2134
+ **Variance-aware subtyping** for generic types respects covariance and contravariance. Covariant type constructors like `List[+A]` preserve subtyping relationships:
2135
+
2136
+ ```scala
2137
+ val listDogId = TypeId.of[List[Dog]]
2138
+ // listDogId: TypeId[List[Dog]] = List[Dog]
2139
+ val listMammalId = TypeId.of[List[Mammal]]
2140
+ // listMammalId: TypeId[List[Mammal]] = List[Mammal]
2141
+ val listAnimalId = TypeId.of[List[Animal]]
2142
+ // listAnimalId: TypeId[List[Animal]] = List[Animal]
2143
+
2144
+ listDogId.isSubtypeOf(listMammalId)
2145
+ // res247: Boolean = true
2146
+ listDogId.isSubtypeOf(listAnimalId)
2147
+ // res248: Boolean = true
2148
+ listMammalId.isSubtypeOf(listAnimalId)
2149
+ // res249: Boolean = true
2150
+ ```
2151
+
2152
+ These methods are essential for building type-safe registries, implementing generic serializers that dispatch based on type hierarchy, and validating API contracts that require specific type relationships.
2153
+
2154
+ ## Annotations
2155
+
2156
+ **Annotations** are metadata attached to types at compile time. TypeId captures them at runtime, making this metadata available for introspection, validation, and dispatch logic. Annotations enable building smart serializers, validators, and code generators that adjust their behavior based on type-level metadata.
2157
+
2158
+ TypeId exposes each annotation as an `Annotation` object containing the annotation's type and its arguments. This is essential for frameworks that need to read compile-time metadata (like JPA, validation libraries, or custom serialization frameworks) but want to remain generic and support multiple annotation schemes:
2159
+
2160
+ ```scala
2161
+ import zio.blocks.typeid._
2162
+
2163
+ @transient
2164
+ case class ImportantData(id: Int, payload: String)
2165
+
2166
+ case class Plain(x: Int)
2167
+ ```
2168
+
2169
+ Inspect annotations on a type:
2170
+
2171
+ ```scala
2172
+ val importantId = TypeId.of[ImportantData]
2173
+ // importantId: TypeId[ImportantData] = ImportantData
2174
+ importantId.annotations
2175
+ // res251: List[Annotation] = List(
2176
+ // Annotation(typeId = transient, args = List())
2177
+ // )
2178
+ importantId.annotations.map(_.name)
2179
+ // res252: List[String] = List("transient")
2180
+
2181
+ TypeId.of[Plain].annotations
2182
+ // res253: List[Annotation] = List()
2183
+ ```
2184
+
2185
+ Annotations can have arguments and parameters. Create a custom annotation to see how arguments are captured:
2186
+
2187
+ ```scala
2188
+ import zio.blocks.typeid._
2189
+
2190
+ // Custom annotation with parameters
2191
+ case class ApiEndpoint(version: Int, deprecated: Boolean = false) extends scala.annotation.StaticAnnotation
2192
+
2193
+ @ApiEndpoint(version = 2, deprecated = true)
2194
+ case class UserV2(id: String, name: String)
2195
+ ```
2196
+
2197
+ Derive the TypeId and inspect annotation arguments:
2198
+
2199
+ ```scala
2200
+ val userV2Id = TypeId.of[UserV2]
2201
+ // userV2Id: TypeId[UserV2] = UserV2
2202
+ userV2Id.annotations
2203
+ // res255: List[Annotation] = List(
2204
+ // Annotation(
2205
+ // typeId = ApiEndpoint,
2206
+ // args = List(
2207
+ // Named(name = "version", value = Const(2)),
2208
+ // Named(name = "deprecated", value = Const(true))
2209
+ // )
2210
+ // )
2211
+ // )
2212
+ userV2Id.annotations.head.args
2213
+ // res256: List[AnnotationArg] = List(
2214
+ // Named(name = "version", value = Const(2)),
2215
+ // Named(name = "deprecated", value = Const(true))
2216
+ // )
2217
+ ```
2218
+
2219
+ Annotations are represented internally as instances of the `Annotation` data class. The `Annotation` contains the annotation's `TypeId` and a list of `AnnotationArg` values representing the arguments:
2220
+
2221
+ | Type | Description |
2222
+ |------------------------------------------------|----------------------------------------------------|
2223
+ | `Annotation(typeId, args)` | An annotation instance with its type and arguments |
2224
+ | `AnnotationArg.Const(value)` | A constant value argument |
2225
+ | `AnnotationArg.Named(name, value)` | A named parameter |
2226
+ | `AnnotationArg.ArrayArg(values)` | An array of arguments |
2227
+ | `AnnotationArg.Nested(annotation)` | A nested annotation |
2228
+ | `AnnotationArg.ClassOf(tpe)` | A `classOf[T]` argument |
2229
+ | `AnnotationArg.EnumValue(enumType, valueName)` | An enum constant |
2230
+
2231
+ **Use cases:** Annotations are commonly used to drive serialization strategies, enforce validation rules, mark types for code generation, configure persistence metadata, or enable framework-specific behavior without requiring explicit configuration objects.
2232
+
2233
+ ## Namespaces and Owners
2234
+
2235
+ Every type has an `owner` — the hierarchical path that tells you where the type is defined (its package, enclosing object, or enclosing type). Owner is essential when you need to identify types by their origin, filter schemas by namespace, or build type-indexed registries that respect module boundaries.
2236
+
2237
+ When building cross-module systems (middleware, gateways, plugin registries, code generators), you often need to distinguish between types from different packages or modules. For example, you might want to:
2238
+ - Apply different serialization strategies to types from `com.internal.domain` vs `com.external.api`
2239
+ - Build a type registry keyed by both type name *and* origin package (to handle name collisions across modules)
2240
+ - Validate that a deserialized type comes from a trusted package
2241
+
2242
+ Owner gives you the tools to make these decisions at runtime.
2243
+
2244
+ ### Inspecting Owners
2245
+
2246
+ When you derive a TypeId, the `owner` property captures where the type is defined:
2247
+
2248
+ ```scala
2249
+ import zio.blocks.typeid._
2250
+
2251
+ case class MyType(x: Int)
2252
+ ```
2253
+
2254
+ ```scala
2255
+ val myId = TypeId.of[MyType]
2256
+ // myId: TypeId[MyType] = MyType
2257
+ myId.owner
2258
+ // res258: Owner = Owner(
2259
+ // List(Package("repl"), Term("MdocSession"), Type("MdocApp257"))
2260
+ // )
2261
+ myId.owner.asString
2262
+ // res259: String = "repl.MdocSession.MdocApp257"
2263
+ myId.fullName
2264
+ // res260: String = "repl.MdocSession.MdocApp257.MyType"
2265
+ ```
2266
+
2267
+ Owner provides methods to inspect and navigate the hierarchy:
2268
+
2269
+ ```scala
2270
+ myId.owner.parent
2271
+ // res261: Owner = Owner(List(Package("repl"), Term("MdocSession")))
2272
+ myId.owner.lastName
2273
+ // res262: String = "MdocApp257"
2274
+ myId.owner.isRoot
2275
+ // res263: Boolean = false
2276
+ ```
2277
+
2278
+ ### Owner Structure: Packages, Terms, and Types
2279
+
2280
+ An Owner is a chain of segments: packages, terms (objects/values), and types. For a type defined as:
2281
+
2282
+ ```scala
2283
+ package com.example
2284
+ object Outer {
2285
+ class Inner
2286
+ }
2287
+ ```
2288
+
2289
+ The owner of `Inner` has three segments: `com`, `example` (packages), and `Outer` (term):
2290
+
2291
+ ```scala
2292
+ import zio.blocks.typeid._
2293
+
2294
+ object ExampleModule {
2295
+ case class Config(timeout: Int)
2296
+ }
2297
+ ```
2298
+
2299
+ ```scala
2300
+ val configId = TypeId.of[ExampleModule.Config]
2301
+ // configId: TypeId[Config] = Config
2302
+ configId.owner.asString
2303
+ // res265: String = "repl.MdocSession.MdocApp264.ExampleModule"
2304
+ ```
2305
+
2306
+ ### TermPath
2307
+
2308
+ `TermPath` represents paths to term values and is used in TypeRepr expressions for singleton types (like `obj.field.type`). Singleton type information exists at compile time but is **erased at runtime** by the JVM — both `TypeId.of[HttpStatus.OK.type]` and `TypeId.of[HttpStatus.NotFound.type]` resolve to the same underlying `Int` TypeId. TermPath is useful in type representation structures and code generators that need to capture the compile-time singleton distinction for metaprogramming.
2309
+
2310
+ When the macro encounters a singleton type (a `TermRef` in Scala's reflection API), it recursively walks the qualifier chain to build the term path and stores it as `TypeRepr.Singleton(path)` for use in type expressions.
2311
+
2312
+ Derive TypeIds for singleton values to see them resolve to their underlying type:
2313
+
2314
+ ```scala
2315
+ import zio.blocks.typeid._
2316
+
2317
+ object HttpStatus {
2318
+ val OK = 200
2319
+ val NotFound = 404
2320
+ }
2321
+ ```
2322
+
2323
+ ```scala
2324
+ val okSingletonId = TypeId.of[HttpStatus.OK.type]
2325
+ // okSingletonId: TypeId[OK] = Int
2326
+ okSingletonId.name
2327
+ // res267: String = "Int"
2328
+
2329
+ val notFoundSingletonId = TypeId.of[HttpStatus.NotFound.type]
2330
+ // notFoundSingletonId: TypeId[NotFound] = Int
2331
+ notFoundSingletonId.name
2332
+ // res268: String = "Int"
2333
+
2334
+ okSingletonId == notFoundSingletonId
2335
+ // res269: Boolean = true
2336
+ ```
2337
+
2338
+ **When to use TermPath:** In TypeRepr expressions and code generators that need to represent the compile-time path to a value. While singleton types are erased at runtime, TermPath captures this distinction for reflection and metaprogramming scenarios.
2339
+
2340
+ ## TypeRepr — Type Expressions
2341
+
2342
+ `TypeRepr` represents type expressions in the Scala type system. This is fundamentally different from `TypeId`: while `TypeId` identifies a specific type *definition* (like `List` as a class or `Person` as a case class), `TypeRepr` represents how types are *composed and expressed* at runtime — as type arguments, parent types, intersections, unions, functions, and more.
2343
+
2344
+ **The Key Distinction:**
2345
+
2346
+ A `TypeId` answers the question **"What is this type definition?"** — for example, `TypeId.of[List]` gives you metadata about the `List` class itself. But a `TypeRepr` answers **"How is this type used in context?"** — for example, when you inspect `TypeId.of[List[Int]].typeArgs`, you get a `TypeRepr.Applied(Ref(TypeId.list), List(Ref(TypeId.int)))`, which describes that `List` is applied to the type argument `Int`.
2347
+
2348
+ **Practical Examples of the Difference:**
2349
+
2350
+ - `TypeId.list` identifies the `List` class definition
2351
+ - `TypeRepr.Ref(TypeId.list)` is the expression "use List as a type" (standalone)
2352
+ - `TypeRepr.Applied(TypeId.list, args)` is the expression "List applied to type arguments" (e.g., `List[Int]`)
2353
+ - `TypeRepr.Function` represents `(A, B) => C` as a first-class type expression (not a method signature)
2354
+ - `TypeRepr.Union` represents `A | B` without requiring a union type definition to exist
2355
+
2356
+ TypeRepr variants like `Intersection`, `Tuple`, `Union`, `TypeLambda`, and `ContextFunction` allow you to represent type expressions that may not have their own `TypeId` definitions — they are *computed expressions* in the type system rather than named definitions.
2357
+
2358
+ You encounter `TypeRepr` values when inspecting `typeArgs`, parent types in `defKind`, and alias targets:
2359
+
2360
+ ```scala
2361
+ import zio.blocks.typeid._
2362
+ ```
2363
+
2364
+ When you derive a TypeId for an applied type, the `typeArgs` are `TypeRepr` values representing the type arguments:
2365
+
2366
+ ```scala
2367
+ TypeId.of[Int & String].typeArgs
2368
+ // res271: List[TypeRepr] = List()
2369
+ TypeId.of[Map[String, Int]].typeArgs
2370
+ // res272: List[TypeRepr] = List(Ref(String), Ref(Int))
2371
+ ```
2372
+
2373
+ Here is a reference of the different `TypeRepr` variants you may encounter when inspecting TypeIds:
2374
+
2375
+ | Category | Variant | Example |
2376
+ |--------------|--------------------------------------------------|---------------------------------------------|
2377
+ | **Common** | `Ref(id)` | `Int`, `String` — reference to a named type |
2378
+ | | `ParamRef(param, depth)` | `A` — reference to a type parameter |
2379
+ | | `Applied(tycon, args)` | `List[Int]` — parameterized type |
2380
+ | **Compound** | `Intersection(types)` | `A & B` (Scala 3) or `A with B` (Scala 2) |
2381
+ | | `Union(types)` | `A \| B` (Scala 3) |
2382
+ | | `Tuple(elems)` | `(A, B, C)`, named tuples |
2383
+ | | `Function(params, result)` | `(A, B) => C` |
2384
+ | | `ContextFunction(params, result)` | `(A, B) ?=> C` (Scala 3) |
2385
+ | **Special** | `Singleton(path)` | `x.type` |
2386
+ | | `ThisType(owner)` | `this.type` |
2387
+ | | `TypeProjection(qualifier, name)` | `Outer#Inner` |
2388
+ | | `TypeSelect(qualifier, name)` | `qual.Member` |
2389
+ | | `Structural(parents, members)` | `{ def foo: Int }` |
2390
+ | **Advanced** | `TypeLambda(params, body)` | `[X] =>> F[X]` |
2391
+ | | `Wildcard(bounds)` | `?`, `? <: Upper` |
2392
+ | | `ByName(underlying)` | `=> A` |
2393
+ | | `Repeated(element)` | `A*` |
2394
+ | | `Annotated(underlying, annotations)` | `A @anno` |
2395
+ | | `Constant.*` | `42`, `"foo"`, `true` (literal types) |
2396
+ | **Builtins** | `AnyType`, `NothingType`, `NullType`, `UnitType` | Special types |
2397
+
2398
+ ## Erased TypeId
2399
+
2400
+ For type-indexed collections where the type parameter doesn't matter, erase it:
2401
+
2402
+ ```scala
2403
+ import zio.blocks.typeid._
2404
+ ```
2405
+
2406
+ ```scala
2407
+ val erased: TypeId.Erased = TypeId.of[Int].erased
2408
+ // erased: TypeId[Unknown] = Int
2409
+ erased
2410
+ // res274: TypeId[Unknown] = Int
2411
+ ```
2412
+
2413
+ Erased TypeIds are the key to building type-indexed maps:
2414
+
2415
+ ```scala
2416
+ val registry: Map[TypeId.Erased, String] = Map(
2417
+ TypeId.of[Int].erased -> "Integer type",
2418
+ TypeId.of[String].erased -> "String type"
2419
+ )
2420
+ // registry: Map[Erased, String] = Map(
2421
+ // Int -> "Integer type",
2422
+ // String -> "String type"
2423
+ // )
2424
+
2425
+ registry.get(TypeId.of[Int].erased)
2426
+ // res275: Option[String] = Some("Integer type")
2427
+ registry.get(TypeId.of[Double].erased)
2428
+ // res276: Option[String] = None
2429
+ ```
2430
+
2431
+ ## Predefined TypeIds
2432
+
2433
+ TypeId provides instances for common types:
2434
+
2435
+ **Core Interfaces:** `TypeId.charSequence` (`java.lang`), `comparable` (`java.lang`), `serializable` (`java.io`)
2436
+
2437
+ **Primitives:** `TypeId.unit`, `boolean`, `byte`, `short`, `int`, `long`, `float`, `double`, `char`, `string`, `bigInt`, `bigDecimal`
2438
+
2439
+ **Collections:** `TypeId.option`, `some`, `none`, `list`, `vector`, `set`, `seq`, `indexedSeq`, `map`, `either`, `array`, `arraySeq`, `chunk`
2440
+
2441
+ **java.time:** `TypeId.dayOfWeek`, `duration`, `instant`, `localDate`, `localDateTime`, `localTime`, `month`, `monthDay`, `offsetDateTime`, `offsetTime`, `period`, `year`, `yearMonth`, `zoneId`, `zoneOffset`, `zonedDateTime`
2442
+
2443
+ **java.util:** `TypeId.currency`, `uuid`
2444
+
2445
+ **Scala 3 only:** `TypeId.iarray` — `IArray[T]`, the immutable array type.
2446
+
2447
+ ## Integration with Schema
2448
+
2449
+ TypeId is central to ZIO Blocks' schema system. Every `Reflect` node carries an associated TypeId:
2450
+
2451
+ ```scala
2452
+ import zio.blocks.schema._
2453
+
2454
+ case class Person(name: String, age: Int)
2455
+ object Person {
2456
+ implicit val schema: Schema[Person] = Schema.derived
2457
+ }
2458
+ ```
2459
+
2460
+ You can access the TypeId from a schema's reflection:
2461
+
2462
+ ```scala
2463
+ val reflect = Schema[Person].reflect
2464
+ val typeId = reflect.typeId
2465
+ typeId.name
2466
+ typeId.isCaseClass
2467
+ ```
2468
+
2469
+ ### Schema Transformations
2470
+
2471
+ TypeId is automatically attached when transforming schemas. The `transform` method takes an implicit `TypeId[B]` parameter, so the TypeId for the target type is resolved at compile time:
2472
+
2473
+ ```scala
2474
+ case class Email(value: String)
2475
+
2476
+ object Email {
2477
+ implicit val schema: Schema[Email] = Schema[String]
2478
+ .transform(Email(_), _.value)
2479
+ // TypeId[Email] is resolved implicitly — no extra call needed
2480
+ }
2481
+ ```
2482
+
2483
+ ### Schema Derivation
2484
+
2485
+ The `Deriver` trait receives a `TypeId` for each node in the schema. Methods like `deriveRecord` and `deriveVariant` include a `typeId: TypeId[A]` parameter alongside fields/cases, bindings, documentation, modifiers, and more. This lets you inspect the type's structure, annotations, and relationships when generating code.
2486
+
2487
+ For details on the full `Deriver` API and how to implement custom derivers, see the [Type Class Derivation](./schema/type-class-derivation.md) reference.
2488
+
2489
+ ## Comparison with Alternatives
2490
+
2491
+ TypeId occupies a different niche from the reflection and type-tagging mechanisms in the Scala ecosystem:
2492
+
2493
+ | Feature | `TypeId` | `ClassTag` | `TypeTag` (Scala 2) | `TypeTest` (Scala 3) | `Mirror` (Scala 3) |
2494
+ |-----------------------------------|----------|------------|---------------------|----------------------|--------------------|
2495
+ | Preserves generic type args | Yes | No | Yes | No | No |
2496
+ | Distinguishes opaque types | Yes | No | No | No | No |
2497
+ | Available on Scala.js | Yes | Partial | No | Yes | Yes |
2498
+ | Cross-version (2 & 3) | Yes | Yes | Scala 2 only | Scala 3 only | Scala 3 only |
2499
+ | Pure data (no runtime reflection) | Yes | No | No | No | Yes |
2500
+ | Captures annotations | Yes | No | Yes | No | No |
2501
+ | Captures variance & kind | Yes | No | Yes | No | No |
2502
+ | Subtype relationship checks | Yes | No | Yes | Yes | No |
2503
+
2504
+ **When to migrate from `ClassTag`:** If you only need `ClassTag` to create arrays of the correct runtime type, keep using it — TypeId does not replace that functionality. If you are using `ClassTag` to identify or dispatch on types, TypeId provides strictly more information (generics, opaque types, annotations) and works identically on JVM and Scala.js.
2505
+
2506
+ **When to migrate from `TypeTag` / `WeakTypeTag`:** These are Scala 2-only, depend on `scala-reflect`, and are not available on Scala.js. TypeId captures comparable metadata (full name, type arguments, variance, annotations) as a pure data structure without runtime reflection, and works across Scala 2, Scala 3, JVM, and Scala.js.
2507
+
2508
+ **When to migrate from `TypeTest`:** `TypeTest` is a Scala 3 mechanism for safe pattern matching on types. It answers "is this value an instance of T?" but does not expose type structure, annotations, or generic arguments. Use TypeId when you need to inspect or serialize type metadata, not just test membership.
2509
+
2510
+ **When to migrate from `Mirror`:** `Mirror` provides structural information about products and sums for derivation in Scala 3. TypeId complements `Mirror` by adding namespace information (owner/package), annotations, opaque type support, and cross-version compatibility. In ZIO Blocks, the schema derivation system uses TypeId rather than `Mirror`.
2511
+
2512
+ ## Running the Examples
2513
+
2514
+ All code from this guide is available as runnable examples in the `schema-examples` module.
2515
+
2516
+ **1. Clone the repository and navigate to the project:**
2517
+
2518
+ ```bash
2519
+ git clone https://github.com/zio/zio-blocks.git
2520
+ cd zio-blocks
2521
+ ```
2522
+
2523
+ **2. Run individual examples with sbt:**
2524
+
2525
+ ### Basic Usage
2526
+
2527
+ Demonstrates deriving TypeIds for case classes, accessing their properties (name, fullName, owner, arity), using predefined TypeIds for built-in types, and implicit derivation:
2528
+
2529
+ ```scala title="schema-examples/src/main/scala/typeid/TypeIdBasicExample.scala"
2530
+ /*
2531
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2532
+ *
2533
+ * Licensed under the Apache License, Version 2.0 (the "License");
2534
+ * you may not use this file except in compliance with the License.
2535
+ * You may obtain a copy of the License at
2536
+ *
2537
+ * http://www.apache.org/licenses/LICENSE-2.0
2538
+ *
2539
+ * Unless required by applicable law or agreed to in writing, software
2540
+ * distributed under the License is distributed on an "AS IS" BASIS,
2541
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2542
+ * See the License for the specific language governing permissions and
2543
+ * limitations under the License.
2544
+ */
2545
+
2546
+ package typeid
2547
+
2548
+ import zio.blocks.typeid.*
2549
+ import zio.sbt.ExprEval.show
2550
+
2551
+ /**
2552
+ * TypeId Basic Example
2553
+ *
2554
+ * Demonstrates deriving TypeIds for case classes, accessing their properties,
2555
+ * and using predefined TypeIds for built-in types.
2556
+ *
2557
+ * Run with: sbt "schema-examples/runMain typeid.TypeIdBasicExample"
2558
+ */
2559
+
2560
+ object TypeIdBasicExample extends App {
2561
+
2562
+ println("═══════════════════════════════════════════════════════════════")
2563
+ println("TypeId Basic Example")
2564
+ println("═══════════════════════════════════════════════════════════════\n")
2565
+
2566
+ // Derive TypeId for a case class
2567
+ case class Person(name: String, age: Int)
2568
+
2569
+ // Simple derivation
2570
+ val personId = TypeId.of[Person]
2571
+
2572
+ println("--- Deriving TypeId for Person ---\n")
2573
+
2574
+ // Accessing name and fullName
2575
+ println("TypeId.of[Person].name")
2576
+ show(personId.name)
2577
+
2578
+ // Accessing fully qualified name
2579
+ println("TypeId.of[Person].fullName")
2580
+ show(personId.fullName)
2581
+
2582
+ // Checking if it's a case class
2583
+ println("TypeId.of[Person].isCaseClass")
2584
+ show(personId.isCaseClass)
2585
+
2586
+ // Accessing the owner (package/enclosing type)
2587
+ println("TypeId.of[Person].owner")
2588
+ show(personId.owner)
2589
+
2590
+ println("\n--- Type Classification ---\n")
2591
+
2592
+ // Check the kind (arity)
2593
+ println("TypeId.of[Person].arity")
2594
+ show(personId.arity)
2595
+
2596
+ // Check it's a proper type
2597
+ println("TypeId.of[Person].isProperType")
2598
+ show(personId.isProperType)
2599
+
2600
+ // Check it's not a type constructor
2601
+ println("TypeId.of[Person].isTypeConstructor")
2602
+ show(personId.isTypeConstructor)
2603
+
2604
+ println("\n--- Predefined TypeIds ---\n")
2605
+
2606
+ // Use predefined TypeIds for built-in types
2607
+ println("TypeId.int.fullName")
2608
+ show(TypeId.int.fullName)
2609
+
2610
+ println("TypeId.string.fullName")
2611
+ show(TypeId.string.fullName)
2612
+
2613
+ println("TypeId.list.name")
2614
+ show(TypeId.list.name)
2615
+
2616
+ // List is a type constructor (arity = 1)
2617
+ println("TypeId.list.arity")
2618
+ show(TypeId.list.arity)
2619
+
2620
+ println("TypeId.map.arity")
2621
+ show(TypeId.map.arity)
2622
+
2623
+ println("\n--- Classification Predicates ---\n")
2624
+
2625
+ // Check various classification predicates
2626
+ println("TypeId.option.isTypeConstructor")
2627
+ show(TypeId.option.isTypeConstructor)
2628
+
2629
+ println("TypeId.either.arity")
2630
+ show(TypeId.either.arity)
2631
+
2632
+ println("\n--- Implicit Derivation ---\n")
2633
+
2634
+ // Implicit derivation
2635
+ val personIdImplicit: TypeId[Person] = implicitly[TypeId[Person]]
2636
+
2637
+ println("implicitly[TypeId[Person]].name")
2638
+ show(personIdImplicit.name)
2639
+
2640
+ println("\n═══════════════════════════════════════════════════════════════")
2641
+
2642
+ show(
2643
+ TypeId.of[scala.util.Either].isSum
2644
+ )
2645
+
2646
+ show(
2647
+ TypeId.of[Option[String]].isOption
2648
+ )
2649
+ show(
2650
+ TypeId.of[Either[String, Int]].isEither
2651
+ )
2652
+
2653
+ show(
2654
+ TypeId.of[Option[String]].isSum
2655
+ )
2656
+
2657
+ show(
2658
+ TypeId.of[Either[String, Int]].isSum
2659
+ )
2660
+
2661
+ show(
2662
+ TypeId.of[(Int, String)].isProduct
2663
+ )
2664
+
2665
+ show(
2666
+ TypeId.of[Tuple2[Int, String]].isProduct
2667
+ )
2668
+
2669
+ show(
2670
+ TypeId.of[Product2[Int, String]].isProduct
2671
+ )
2672
+
2673
+ show(
2674
+ TypeId.of[(Int, String, Boolean)].isTuple
2675
+ )
2676
+
2677
+ import zio.blocks.typeid._
2678
+
2679
+ sealed trait Animal
2680
+
2681
+ sealed trait Mammal extends Animal
2682
+
2683
+ case class Dog(name: String) extends Mammal
2684
+
2685
+ case class Cat(name: String) extends Mammal
2686
+
2687
+ case class Fish(species: String) extends Animal
2688
+
2689
+ val dogId = TypeId.of[Dog]
2690
+ val mammalId = TypeId.of[Mammal]
2691
+ val animalId = TypeId.of[Animal]
2692
+ val fishId = TypeId.of[Fish]
2693
+
2694
+ // Direct inheritance: Dog extends Mammal
2695
+ dogId.isSubtypeOf(mammalId)
2696
+
2697
+ // Transitive inheritance: Dog extends Mammal extends Animal
2698
+ dogId.isSubtypeOf(animalId)
2699
+
2700
+ // Not a subtype relationship
2701
+ dogId.isSubtypeOf(fishId)
2702
+ fishId.isSubtypeOf(mammalId)
2703
+
2704
+ TypeId.of[List[Dog]].isSubtypeOf(TypeId.of[List[Mammal]])
2705
+ TypeId.of[List[Dog]].isSubtypeOf(TypeId.of[List[Animal]])
2706
+
2707
+ show(
2708
+ TypeId.of[Mammal => String].isSupertypeOf(TypeId.of[Dog => String])
2709
+ )
2710
+
2711
+ // A generic parent type with concrete type arguments
2712
+ case class StringList() extends scala.collection.mutable.ListBuffer[String]
2713
+
2714
+ // A type with multiple generic parents
2715
+ case class Entry[K, V](key: K, value: V) extends scala.collection.Map[K, V] {
2716
+ def iterator = Iterator((key, value))
2717
+
2718
+ def get(k: K) = if (k == key) Some(value) else None
2719
+
2720
+ override def -(key: K): collection.Map[K, V] = ???
2721
+
2722
+ override def -(key1: K, key2: K, keys: K*): collection.Map[K, V] = ???
2723
+ }
2724
+
2725
+ val stringListId = TypeId.of[StringList]
2726
+ // StringList extends ListBuffer[String] - the type argument is captured
2727
+ show(
2728
+ stringListId.parents
2729
+ )
2730
+
2731
+ val entryId = TypeId.of[Entry[String, Int]]
2732
+ // Entry[String, Int] extends Map[String, Int] - type arguments are preserved
2733
+
2734
+ show(
2735
+ entryId.parents
2736
+ )
2737
+
2738
+ import zio.blocks.typeid._
2739
+
2740
+ // A trait can extend multiple traits
2741
+ trait Swimmer {
2742
+ def swim(): Unit = ()
2743
+ }
2744
+
2745
+ trait Flyer {
2746
+ def fly(): Unit = ()
2747
+ }
2748
+
2749
+ trait Duck extends Swimmer with Flyer
2750
+
2751
+ // A case class can extend a trait
2752
+ case class MallardDuck() extends Duck
2753
+
2754
+ show(
2755
+ TypeId.of[MallardDuck].parents
2756
+ )
2757
+
2758
+ def isPrimitive(id: TypeId[?]): Boolean =
2759
+ id.classTag != scala.reflect.ClassTag.AnyRef
2760
+
2761
+ show {
2762
+ isPrimitive(TypeId.of[Int])
2763
+ isPrimitive(TypeId.of[Double])
2764
+ isPrimitive(TypeId.of[Boolean])
2765
+ isPrimitive(TypeId.of[String])
2766
+ isPrimitive(TypeId.of[List[Int]])
2767
+ }
2768
+
2769
+ def makeStorage(size: Int, id: TypeId[?]): Array[?] =
2770
+ id.classTag.newArray(size)
2771
+
2772
+ show {
2773
+ makeStorage(100, TypeId.int).getClass.getComponentType
2774
+ makeStorage(100, TypeId.double).getClass.getComponentType
2775
+ makeStorage(100, TypeId.string).getClass.getComponentType
2776
+ }
2777
+
2778
+ }
2779
+ ```
2780
+
2781
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/TypeIdBasicExample.scala))
2782
+
2783
+ ```bash
2784
+ sbt "schema-examples/runMain typeid.TypeIdBasicExample"
2785
+ ```
2786
+
2787
+ ### Subtype Relationships
2788
+
2789
+ Demonstrates subtype checking with `isSubtypeOf`, `isSupertypeOf`, and `isEquivalentTo`, including direct inheritance, transitive inheritance, sealed trait cases, and variance-aware subtyping for applied types like `List[Dog] <: List[Animal]`:
2790
+
2791
+ ```scala title="schema-examples/src/main/scala/typeid/TypeIdSubtypingExample.scala"
2792
+ /*
2793
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2794
+ *
2795
+ * Licensed under the Apache License, Version 2.0 (the "License");
2796
+ * you may not use this file except in compliance with the License.
2797
+ * You may obtain a copy of the License at
2798
+ *
2799
+ * http://www.apache.org/licenses/LICENSE-2.0
2800
+ *
2801
+ * Unless required by applicable law or agreed to in writing, software
2802
+ * distributed under the License is distributed on an "AS IS" BASIS,
2803
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2804
+ * See the License for the specific language governing permissions and
2805
+ * limitations under the License.
2806
+ */
2807
+
2808
+ package typeid
2809
+
2810
+ import zio.blocks.typeid._
2811
+ import zio.sbt.ExprEval.show
2812
+
2813
+ /**
2814
+ * TypeId Subtyping Example
2815
+ *
2816
+ * Demonstrates subtype relationships, checking inheritance, and variance-aware
2817
+ * subtyping for applied types.
2818
+ *
2819
+ * Run with: sbt "schema-examples/runMain typeid.TypeIdSubtypingExample"
2820
+ */
2821
+
2822
+ object TypeIdSubtypingExample extends App {
2823
+
2824
+ println("═══════════════════════════════════════════════════════════════")
2825
+ println("TypeId Subtyping Example")
2826
+ println("═══════════════════════════════════════════════════════════════\n")
2827
+
2828
+ // Define a sealed trait hierarchy
2829
+ sealed trait Animal
2830
+ case class Dog(name: String) extends Animal
2831
+ case class Cat(name: String) extends Animal
2832
+ case object Bird extends Animal
2833
+
2834
+ println("--- Direct Inheritance ---\n")
2835
+
2836
+ val dogId = TypeId.of[Dog]
2837
+ val animalId = TypeId.of[Animal]
2838
+
2839
+ // Check if Dog is a subtype of Animal
2840
+ println("TypeId.of[Dog].isSubtypeOf(TypeId.of[Animal])")
2841
+ show(dogId.isSubtypeOf(animalId))
2842
+
2843
+ // Check if Animal is a supertype of Dog
2844
+ println("TypeId.of[Animal].isSupertypeOf(TypeId.of[Dog])")
2845
+ show(animalId.isSupertypeOf(dogId))
2846
+
2847
+ // Check equivalence
2848
+ println("TypeId.of[Dog].isEquivalentTo(TypeId.of[Dog])")
2849
+ show(dogId.isEquivalentTo(dogId))
2850
+
2851
+ println("TypeId.of[Dog].isEquivalentTo(TypeId.of[Animal])")
2852
+ show(dogId.isEquivalentTo(animalId))
2853
+
2854
+ println("\n--- Transitive Inheritance ---\n")
2855
+
2856
+ val catId = TypeId.of[Cat]
2857
+
2858
+ // Both Dog and Cat are subtypes of Animal
2859
+ println("TypeId.of[Cat].isSubtypeOf(TypeId.of[Animal])")
2860
+ show(catId.isSubtypeOf(animalId))
2861
+
2862
+ println("TypeId.of[Dog].isSubtypeOf(TypeId.of[Animal]) && TypeId.of[Cat].isSubtypeOf(TypeId.of[Animal])")
2863
+ show(dogId.isSubtypeOf(animalId) && catId.isSubtypeOf(animalId))
2864
+
2865
+ println("\n--- Sealed Trait Cases ---\n")
2866
+
2867
+ val birdId = TypeId.of[Bird.type]
2868
+
2869
+ println("TypeId.of[Bird.type].isSubtypeOf(TypeId.of[Animal])")
2870
+ show(birdId.isSubtypeOf(animalId))
2871
+
2872
+ println("\n--- Applied Type Subtyping (Covariance) ---\n")
2873
+
2874
+ // List is covariant in its type parameter
2875
+ val listDogId = TypeId.of[List[Dog]]
2876
+ val listAnimalId = TypeId.of[List[Animal]]
2877
+
2878
+ // Due to covariance, List[Dog] is a subtype of List[Animal]
2879
+ println("TypeId.of[List[Dog]].isSubtypeOf(TypeId.of[List[Animal]])")
2880
+ show(listDogId.isSubtypeOf(listAnimalId))
2881
+
2882
+ println("\n--- Applied Type Non-Subtyping (Invariance) ---\n")
2883
+
2884
+ // Array is invariant in its type parameter (on Scala runtime)
2885
+ val arrayDogId = TypeId.of[Array[Dog]]
2886
+ val arrayAnimalId = TypeId.of[Array[Animal]]
2887
+
2888
+ println("TypeId.of[Array[Dog]].isSubtypeOf(TypeId.of[Array[Animal]])")
2889
+ show(arrayDogId.isSubtypeOf(arrayAnimalId))
2890
+
2891
+ println("\n--- Type Parameters ---\n")
2892
+
2893
+ // Check the type parameters of a generic type
2894
+ println("TypeId.of[List[Dog]].typeArgs")
2895
+ show(listDogId.typeArgs)
2896
+
2897
+ println("TypeId.of[Map[String, Animal]].typeArgs")
2898
+ show(TypeId.of[Map[String, Animal]].typeArgs)
2899
+
2900
+ println("\n═══════════════════════════════════════════════════════════════")
2901
+ }
2902
+ ```
2903
+
2904
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/TypeIdSubtypingExample.scala))
2905
+
2906
+ ```bash
2907
+ sbt "schema-examples/runMain typeid.TypeIdSubtypingExample"
2908
+ ```
2909
+
2910
+ ### Normalization and Registries
2911
+
2912
+ Demonstrates type alias handling, normalization to underlying types, structural equality, and building type-indexed registries using erased TypeIds:
2913
+
2914
+ ```scala title="schema-examples/src/main/scala/typeid/TypeIdNormalizationExample.scala"
2915
+ /*
2916
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2917
+ *
2918
+ * Licensed under the Apache License, Version 2.0 (the "License");
2919
+ * you may not use this file except in compliance with the License.
2920
+ * You may obtain a copy of the License at
2921
+ *
2922
+ * http://www.apache.org/licenses/LICENSE-2.0
2923
+ *
2924
+ * Unless required by applicable law or agreed to in writing, software
2925
+ * distributed under the License is distributed on an "AS IS" BASIS,
2926
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2927
+ * See the License for the specific language governing permissions and
2928
+ * limitations under the License.
2929
+ */
2930
+
2931
+ package typeid
2932
+
2933
+ import zio.blocks.typeid._
2934
+ import zio.sbt.ExprEval.show
2935
+
2936
+ /**
2937
+ * TypeId Normalization Example
2938
+ *
2939
+ * Demonstrates type alias handling, normalization, and type-indexed registries
2940
+ * using erased TypeIds.
2941
+ *
2942
+ * Run with: sbt "schema-examples/runMain typeid.TypeIdNormalizationExample"
2943
+ */
2944
+
2945
+ object TypeIdNormalizationExample extends App {
2946
+
2947
+ println("═══════════════════════════════════════════════════════════════")
2948
+ println("TypeId Normalization Example")
2949
+ println("═══════════════════════════════════════════════════════════════\n")
2950
+
2951
+ // Define some type aliases
2952
+ type UserId = Long
2953
+ type Email = String
2954
+ type Age = Int
2955
+
2956
+ println("--- Type Aliases ---\n")
2957
+
2958
+ // Create TypeIds for the aliases
2959
+ val userIdAlias = TypeId.alias[UserId](
2960
+ name = "UserId",
2961
+ owner = Owner.Root,
2962
+ typeParams = Nil,
2963
+ aliased = TypeRepr.Ref(TypeId.long)
2964
+ )
2965
+
2966
+ show(userIdAlias.name)
2967
+
2968
+ show(userIdAlias.fullName)
2969
+
2970
+ println("\n--- Normalization ---\n")
2971
+
2972
+ // Normalize the alias to its underlying type
2973
+ val normalized = TypeId.normalize(userIdAlias)
2974
+
2975
+ show(normalized.name)
2976
+
2977
+ show(normalized.fullName)
2978
+
2979
+ println("\n--- Equality with Normalization ---\n")
2980
+
2981
+ // Structural equality (not considering aliases)
2982
+ val anotherUserIdAlias = TypeId.alias[UserId](
2983
+ name = "UserId",
2984
+ owner = Owner.Root,
2985
+ typeParams = Nil,
2986
+ aliased = TypeRepr.Ref(TypeId.long)
2987
+ )
2988
+
2989
+ show(userIdAlias == anotherUserIdAlias)
2990
+
2991
+ show(TypeId.normalize(userIdAlias) == TypeId.long)
2992
+
2993
+ println("\n--- Erased TypeIds for Registries ---\n")
2994
+
2995
+ // Erased TypeIds are useful for type-indexed maps
2996
+ val intErased: TypeId.Erased = TypeId.int.erased
2997
+ val stringErased: TypeId.Erased = TypeId.string.erased
2998
+ val listIntErased: TypeId.Erased = TypeId.of[List[Int]].erased
2999
+
3000
+ show(intErased)
3001
+
3002
+ show(stringErased)
3003
+
3004
+ show(listIntErased)
3005
+
3006
+ println("\n--- Type Registry Using Erased TypeIds ---\n")
3007
+
3008
+ // Build a type registry
3009
+ val registry: Map[TypeId.Erased, String] = Map(
3010
+ TypeId.int.erased -> "Integer type",
3011
+ TypeId.string.erased -> "String type",
3012
+ TypeId.long.erased -> "Long type",
3013
+ TypeId.of[List[Int]].erased -> "List of integers"
3014
+ )
3015
+
3016
+ show(registry.get(TypeId.int.erased))
3017
+
3018
+ show(registry.get(TypeId.string.erased))
3019
+
3020
+ show(registry.get(TypeId.of[List[Int]].erased))
3021
+
3022
+ println("\n--- Querying the Registry ---\n")
3023
+
3024
+ // Look up types in the registry
3025
+ val intType = TypeId.of[Int].erased
3026
+ val doubleType = TypeId.of[Double].erased
3027
+
3028
+ show(registry.get(intType))
3029
+
3030
+ show(registry.get(doubleType))
3031
+
3032
+ println("\n═══════════════════════════════════════════════════════════════")
3033
+ }
3034
+ ```
3035
+
3036
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/TypeIdNormalizationExample.scala))
3037
+
3038
+ ```bash
3039
+ sbt "schema-examples/runMain typeid.TypeIdNormalizationExample"
3040
+ ```
3041
+
3042
+ ### Opaque Types
3043
+
3044
+ Demonstrates how TypeId preserves the semantic distinction of opaque types, enabling runtime type safety that pure Scala reflection cannot provide. Shows building type-indexed validator registries keyed by opaque type identity:
3045
+
3046
+ ```scala title="schema-examples/src/main/scala/typeid/OpaqueTypesExample.scala"
3047
+ /*
3048
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
3049
+ *
3050
+ * Licensed under the Apache License, Version 2.0 (the "License");
3051
+ * you may not use this file except in compliance with the License.
3052
+ * You may obtain a copy of the License at
3053
+ *
3054
+ * http://www.apache.org/licenses/LICENSE-2.0
3055
+ *
3056
+ * Unless required by applicable law or agreed to in writing, software
3057
+ * distributed under the License is distributed on an "AS IS" BASIS,
3058
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3059
+ * See the License for the specific language governing permissions and
3060
+ * limitations under the License.
3061
+ */
3062
+
3063
+ package typeid
3064
+
3065
+ import zio.blocks.typeid._
3066
+ import zio.sbt.ExprEval.show
3067
+
3068
+ /**
3069
+ * Opaque Types Example
3070
+ *
3071
+ * Demonstrates how TypeId preserves the semantic distinction of opaque types,
3072
+ * enabling runtime type safety that pure Scala reflection cannot provide.
3073
+ *
3074
+ * Opaque types allow you to create distinct types that share the same
3075
+ * representation at runtime. TypeId captures this distinction, making it
3076
+ * possible to:
3077
+ * - Distinguish between different opaque types wrapping the same base type
3078
+ * - Build type-indexed registries that respect opaque type boundaries
3079
+ * - Implement runtime validators specific to each opaque type
3080
+ *
3081
+ * Run with: sbt "schema-examples/runMain typeid.OpaqueTypesExample"
3082
+ */
3083
+
3084
+ object OpaqueTypesExample extends App {
3085
+
3086
+ println("═══════════════════════════════════════════════════════════════")
3087
+ println("Opaque Types with TypeId")
3088
+ println("═══════════════════════════════════════════════════════════════\n")
3089
+
3090
+ // Define domain-specific opaque types wrapping String
3091
+ // In a real application, these would enforce different validation rules
3092
+ opaque type UserId = String
3093
+ opaque type Email = String
3094
+ opaque type SessionToken = String
3095
+
3096
+ println("--- Opaque Type Definitions ---\n")
3097
+ println("opaque type UserId = String")
3098
+ println("opaque type Email = String")
3099
+ println("opaque type SessionToken = String\n")
3100
+
3101
+ // Derive TypeIds for each opaque type
3102
+ val userIdType = TypeId.of[UserId]
3103
+ val emailType = TypeId.of[Email]
3104
+ val sessionTokenType = TypeId.of[SessionToken]
3105
+ val stringType = TypeId.string
3106
+
3107
+ println("--- TypeId Derivation ---\n")
3108
+
3109
+ println("TypeId.of[UserId].name")
3110
+ show(userIdType.name)
3111
+
3112
+ println("TypeId.of[Email].name")
3113
+ show(emailType.name)
3114
+
3115
+ println("TypeId.of[SessionToken].name")
3116
+ show(sessionTokenType.name)
3117
+
3118
+ println("\n--- Opaque Types vs Base Type ---\n")
3119
+
3120
+ // Key insight: opaque types are distinct from their representation type
3121
+ println("TypeId.of[UserId].isEquivalentTo(TypeId.string)")
3122
+ show(userIdType.isEquivalentTo(stringType))
3123
+
3124
+ println("TypeId.of[Email].isEquivalentTo(TypeId.string)")
3125
+ show(emailType.isEquivalentTo(stringType))
3126
+
3127
+ println("TypeId.of[SessionToken].isEquivalentTo(TypeId.string)")
3128
+ show(sessionTokenType.isEquivalentTo(stringType))
3129
+
3130
+ println("\n--- Opaque Types are Distinct from Each Other ---\n")
3131
+
3132
+ println("TypeId.of[UserId].isEquivalentTo(TypeId.of[Email])")
3133
+ show(userIdType.isEquivalentTo(emailType))
3134
+
3135
+ println("TypeId.of[Email].isEquivalentTo(TypeId.of[SessionToken])")
3136
+ show(emailType.isEquivalentTo(sessionTokenType))
3137
+
3138
+ println("TypeId.of[UserId].isEquivalentTo(TypeId.of[SessionToken])")
3139
+ show(userIdType.isEquivalentTo(sessionTokenType))
3140
+
3141
+ println("\n--- Real-World Use Case: Type-Safe Registry ---\n")
3142
+
3143
+ // Define validators for each opaque type
3144
+ trait Validator {
3145
+ def validate(value: String): Boolean
3146
+ def errorMessage: String
3147
+ }
3148
+
3149
+ val userIdValidator = new Validator {
3150
+ def validate(value: String): Boolean = value.nonEmpty && value.forall(_.isDigit)
3151
+ def errorMessage = "UserId must be non-empty digits"
3152
+ }
3153
+
3154
+ val emailValidator = new Validator {
3155
+ def validate(value: String): Boolean = value.contains("@") && value.contains(".")
3156
+ def errorMessage = "Email must contain @ and ."
3157
+ }
3158
+
3159
+ val sessionTokenValidator = new Validator {
3160
+ def validate(value: String): Boolean = value.length >= 32
3161
+ def errorMessage = "SessionToken must be at least 32 characters"
3162
+ }
3163
+
3164
+ // Build a type-indexed registry of validators
3165
+ // This demonstrates the power of TypeId: we can safely dispatch
3166
+ // to different validators based on opaque type identity
3167
+ val validatorRegistry: Map[TypeId.Erased, Validator] = Map(
3168
+ TypeId.of[UserId].erased -> userIdValidator,
3169
+ TypeId.of[Email].erased -> emailValidator,
3170
+ TypeId.of[SessionToken].erased -> sessionTokenValidator
3171
+ )
3172
+
3173
+ println("Built validator registry keyed by opaque type")
3174
+ println("Validators can enforce different validation rules per type\n")
3175
+
3176
+ // Demonstrate validation dispatch
3177
+ def validateString(value: String, typeId: TypeId[_]): Boolean =
3178
+ validatorRegistry
3179
+ .get(typeId.erased)
3180
+ .map(_.validate(value))
3181
+ .getOrElse {
3182
+ println(s"No validator found for type: ${typeId.fullName}")
3183
+ false
3184
+ }
3185
+
3186
+ def getValidationError(typeId: TypeId[_]): String =
3187
+ validatorRegistry
3188
+ .get(typeId.erased)
3189
+ .map(_.errorMessage)
3190
+ .getOrElse("Unknown validator")
3191
+
3192
+ println("--- Validation Examples ---\n")
3193
+
3194
+ val testUserId = "12345"
3195
+ val testEmail = "user@example.com"
3196
+ val testToken = "a" * 32
3197
+
3198
+ println(s"Validating UserId: '$testUserId'")
3199
+ if (validateString(testUserId, TypeId.of[UserId])) {
3200
+ println("✓ Valid UserId\n")
3201
+ } else {
3202
+ println(s"✗ Invalid: ${getValidationError(TypeId.of[UserId])}\n")
3203
+ }
3204
+
3205
+ println(s"Validating Email: '$testEmail'")
3206
+ if (validateString(testEmail, TypeId.of[Email])) {
3207
+ println("✓ Valid Email\n")
3208
+ } else {
3209
+ println(s"✗ Invalid: ${getValidationError(TypeId.of[Email])}\n")
3210
+ }
3211
+
3212
+ println(s"Validating SessionToken: '$testToken'")
3213
+ if (validateString(testToken, TypeId.of[SessionToken])) {
3214
+ println("✓ Valid SessionToken\n")
3215
+ } else {
3216
+ println(s"✗ Invalid: ${getValidationError(TypeId.of[SessionToken])}\n")
3217
+ }
3218
+
3219
+ println("--- What Pure Scala Cannot Do ---\n")
3220
+
3221
+ println("With pure Scala reflection:")
3222
+ println("- classOf[UserId] == classOf[String] (erased at runtime)")
3223
+ println("- classOf[Email] == classOf[String] (erased at runtime)")
3224
+ println("- You cannot distinguish opaque types from their base type\n")
3225
+
3226
+ println("With TypeId:")
3227
+ println("- TypeId.of[UserId] != TypeId.of[String] (preserved)")
3228
+ println("- TypeId.of[Email] != TypeId.of[String] (preserved)")
3229
+ println("- You can build type-safe validators and registries\n")
3230
+
3231
+ println("═══════════════════════════════════════════════════════════════")
3232
+ }
3233
+ ```
3234
+
3235
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/OpaqueTypesExample.scala))
897
3236
 
898
- // Construct instances (JVM only)
899
- val result: Either[String, Any] = typeId.construct(Chunk("Alice", 30))
3237
+ ```bash
3238
+ sbt "schema-examples/runMain typeid.OpaqueTypesExample"
900
3239
  ```