@zio.dev/zio-blocks 0.0.31 → 0.0.33
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.
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +1 -1
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +13 -13
- package/package.json +1 -1
- package/reference/codec.md +27 -19
- package/reference/context.md +1 -1
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +7 -7
- package/reference/formats.md +15 -11
- package/reference/json-patch.md +2 -2
- package/reference/media-type.md +2 -2
- package/reference/resource-management/resource.md +2 -2
- package/reference/resource-management/scope.md +1 -1
- package/reference/resource-management/wire.md +2 -2
- package/reference/schema-evolution/as.md +7 -7
- package/reference/schema-evolution/into.md +7 -7
- package/reference/schema-expr.md +2 -2
- package/reference/schema.md +1 -1
- package/reference/type-class-derivation.md +1 -1
- package/reference/typeid.md +2936 -583
- package/reference/xml.md +25 -188
- package/ringbuffer.md +1 -1
- package/superpowers/plans/2026-03-19-docs-critique-subagent.md +407 -0
- package/superpowers/specs/2026-03-19-docs-critique-subagent-design.md +222 -0
- package/undocumented-report.md +5 -5
package/reference/typeid.md
CHANGED
|
@@ -3,898 +3,3251 @@ id: typeid
|
|
|
3
3
|
title: "TypeId"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
+
The `TypeId` trait exposes the type's structure through a rich set of properties and predicates:
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
personId.fullName // "com.example.Person"
|
|
29
|
-
personId.isCaseClass // true
|
|
45
|
+
val id = TypeId.of[Person]
|
|
46
|
+
```
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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" % "
|
|
68
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-typeid" % "@VERSION"
|
|
43
69
|
```
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
For cross-platform (Scala.js):
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
```scala
|
|
74
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-typeid" % "@VERSION"
|
|
75
|
+
```
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
Supported Scala versions: 2.13.x and 3.x.
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
import zio.blocks.typeid._
|
|
79
|
+
## Creating Instances
|
|
55
80
|
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
val userId: TypeId[User] = TypeId.of[User]
|
|
83
|
+
### Automatic Derivation
|
|
60
84
|
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
val userId: TypeId[User] = implicitly[TypeId[User]]
|
|
66
|
-
```
|
|
87
|
+
#### `TypeId.of` — Macro Derivation
|
|
67
88
|
|
|
68
|
-
The macro extracts complete type
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
Derive a TypeId using the macro:
|
|
77
99
|
|
|
78
100
|
```scala
|
|
79
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
val
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
117
|
+
#### `TypeId.derived` — Implicit Derivation
|
|
102
118
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
TypeId
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
+
The most common use case is accepting `TypeId[A]` as an implicit parameter:
|
|
123
135
|
|
|
124
136
|
```scala
|
|
125
|
-
|
|
137
|
+
import zio.blocks.typeid._
|
|
126
138
|
|
|
127
|
-
id
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
145
|
+
Call the function with the type argument — the TypeId is derived automatically:
|
|
136
146
|
|
|
137
147
|
```scala
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
154
|
+
You can also summon a TypeId explicitly with `implicitly` (Scala 2) or `summon` (Scala 3):
|
|
155
155
|
|
|
156
156
|
```scala
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
sealed trait Animal
|
|
168
|
-
case class Dog(name: String) extends Animal
|
|
165
|
+
### Manual Derivation (Smart Constructors)
|
|
169
166
|
|
|
170
|
-
|
|
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
|
-
|
|
174
|
-
animalId.isSupertypeOf(dogId) // true
|
|
175
|
-
dogId.isEquivalentTo(dogId) // true
|
|
176
|
-
```
|
|
169
|
+
#### `TypeId.nominal` — Nominal Types
|
|
177
170
|
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
If you do need to construct nominal TypeIds manually, the API provides two overloads:
|
|
188
176
|
|
|
189
177
|
```scala
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
191
|
+
#### `TypeId.alias` — Type Aliases
|
|
209
192
|
|
|
210
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
TypeRepr.Ref(TypeId.list),
|
|
230
|
-
List(TypeRepr.Ref(TypeId.int))
|
|
231
|
-
)
|
|
212
|
+
import zio.blocks.typeid._
|
|
213
|
+
```
|
|
232
214
|
|
|
233
|
-
|
|
234
|
-
TypeRepr.
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
224
|
+
#### `TypeId.opaque` — Opaque Types
|
|
241
225
|
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
245
|
+
#### `TypeId.applied` — Applied Types
|
|
257
246
|
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
317
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
318
|
+
def owner: Owner
|
|
319
|
+
}
|
|
320
|
+
```
|
|
315
321
|
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
320
|
-
|
|
327
|
+
case class User(id: Long, name: String)
|
|
328
|
+
```
|
|
321
329
|
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
|
|
397
|
+
sealed trait Container[+A]
|
|
398
|
+
case class Box[+A](value: A) extends Container[A]
|
|
366
399
|
|
|
367
|
-
|
|
368
|
-
|
|
400
|
+
sealed trait Sink[-T]
|
|
401
|
+
case class Logger[-T]() extends Sink[T]
|
|
369
402
|
|
|
370
|
-
|
|
371
|
-
|
|
403
|
+
sealed trait Cache[K, +V]
|
|
404
|
+
case class LRUCache[K, +V](maxSize: Int) extends Cache[K, V]
|
|
372
405
|
```
|
|
373
406
|
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
+
sealed trait Functor[F[_]]
|
|
490
|
+
```
|
|
390
491
|
|
|
391
|
-
|
|
492
|
+
To inspect individual fields of a type parameter, extract and examine each property:
|
|
392
493
|
|
|
393
494
|
```scala
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
399
|
-
val owner = Owner.Root / "com" / "example"
|
|
520
|
+
`TypeParam` provides convenience predicates for checking variance without inspecting the raw `variance` field:
|
|
400
521
|
|
|
401
|
-
|
|
402
|
-
|
|
522
|
+
```scala
|
|
523
|
+
import zio.blocks.typeid._
|
|
403
524
|
|
|
404
|
-
|
|
405
|
-
|
|
525
|
+
sealed trait Box[+A]
|
|
526
|
+
sealed trait Sink[-T]
|
|
527
|
+
sealed trait Cache[K, +V]
|
|
406
528
|
```
|
|
407
529
|
|
|
408
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
566
|
+
#### `typeArgs` — Applied Type Arguments
|
|
418
567
|
|
|
419
|
-
TypeId
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
-
|
|
580
|
+
Setup some custom generic types with different type argument patterns:
|
|
433
581
|
|
|
434
582
|
```scala
|
|
435
|
-
|
|
436
|
-
val path = TermPath.fromOwner(
|
|
437
|
-
Owner.fromPackagePath("com.example").term("MyObject"),
|
|
438
|
-
"value"
|
|
439
|
-
)
|
|
583
|
+
import zio.blocks.typeid._
|
|
440
584
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
590
|
+
Now inspect the type arguments of various applied types:
|
|
447
591
|
|
|
448
|
-
|
|
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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
TypeParam.contravariant("A", 0)
|
|
638
|
+
```scala
|
|
639
|
+
import zio.blocks.typeid._
|
|
463
640
|
|
|
464
|
-
//
|
|
465
|
-
|
|
641
|
+
// Union types (Scala 3)
|
|
642
|
+
case class Handler[T](process: T | String)
|
|
466
643
|
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
//
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
656
|
+
Now inspect the type arguments in these complex types:
|
|
482
657
|
|
|
483
658
|
```scala
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
686
|
+
#### `arity` — Number of Type Parameters
|
|
500
687
|
|
|
501
|
-
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
TypeBounds.upper(upperType)
|
|
691
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
692
|
+
def arity: Int
|
|
693
|
+
}
|
|
694
|
+
```
|
|
509
695
|
|
|
510
|
-
|
|
511
|
-
TypeBounds.lower(lowerType)
|
|
696
|
+
Setup some generic types with different arities:
|
|
512
697
|
|
|
513
|
-
|
|
514
|
-
|
|
698
|
+
```scala
|
|
699
|
+
import zio.blocks.typeid._
|
|
515
700
|
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
707
|
+
Check the arity of different types:
|
|
521
708
|
|
|
522
709
|
```scala
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
-
|
|
537
|
-
Variance.Contravariant // -A
|
|
538
|
-
Variance.Invariant // A
|
|
731
|
+
import zio.blocks.typeid._
|
|
539
732
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
Represents the "kind" of a type (type of types):
|
|
738
|
+
Check which types are proper types:
|
|
551
739
|
|
|
552
740
|
```scala
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
560
|
-
Kind.constructor(1) // * -> *
|
|
561
|
-
Kind.constructor(2) // * -> * -> *
|
|
760
|
+
#### `isTypeConstructor` — Has Type Parameters
|
|
562
761
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
772
|
+
Identify which types are type constructors:
|
|
569
773
|
|
|
570
774
|
```scala
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
794
|
+
#### `isApplied` — Has Type Arguments
|
|
576
795
|
|
|
577
|
-
|
|
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
|
-
|
|
581
|
-
Member.Val("x", TypeRepr.Ref(TypeId.int))
|
|
799
|
+
import zio.blocks.typeid._
|
|
582
800
|
|
|
583
|
-
|
|
584
|
-
|
|
801
|
+
case class Single[A](value: A)
|
|
802
|
+
case class Pair[A, B](a: A, b: B)
|
|
585
803
|
```
|
|
586
804
|
|
|
587
|
-
|
|
805
|
+
Check which types are applied:
|
|
588
806
|
|
|
589
807
|
```scala
|
|
590
|
-
//
|
|
591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
843
|
+
Define types representing different classifications:
|
|
614
844
|
|
|
615
845
|
```scala
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
620
|
-
Member.TypeMember("T", upperBound = Some(upperType))
|
|
856
|
+
Inspect the `defKind` for each type to see how they're classified:
|
|
621
857
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
896
|
+
Use the classification predicates to identify type kinds:
|
|
634
897
|
|
|
635
898
|
```scala
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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 (not in `scala.util` subpackage), 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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1010
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
1011
|
+
def isSubtypeOf(other: TypeId[?]): Boolean
|
|
1012
|
+
}
|
|
660
1013
|
```
|
|
661
1014
|
|
|
662
|
-
|
|
1015
|
+
Define a type hierarchy with direct and transitive relationships:
|
|
663
1016
|
|
|
664
1017
|
```scala
|
|
665
|
-
|
|
666
|
-
bases = List(...)
|
|
667
|
-
)
|
|
1018
|
+
import zio.blocks.typeid._
|
|
668
1019
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
1027
|
+
Check subtyping relationships:
|
|
677
1028
|
|
|
678
1029
|
```scala
|
|
679
|
-
|
|
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
|
-
|
|
682
|
-
publicBounds = TypeBounds.Unbounded // Bounds visible outside
|
|
683
|
-
)
|
|
1054
|
+
Covariant type constructors preserve subtyping relationships:
|
|
684
1055
|
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
1084
|
+
In Scala 3, `isSubtypeOf` correctly handles these advanced type cases:
|
|
705
1085
|
|
|
706
1086
|
```scala
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1110
|
+
#### `isSupertypeOf` — Check Supertyping
|
|
718
1111
|
|
|
719
|
-
|
|
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
|
|
723
|
-
TypeId
|
|
724
|
-
|
|
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
|
-
|
|
1120
|
+
Check supertyping relationships using the same hierarchy:
|
|
737
1121
|
|
|
738
1122
|
```scala
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
TypeId.
|
|
748
|
-
TypeId.
|
|
749
|
-
TypeId.
|
|
750
|
-
TypeId.
|
|
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
|
-
|
|
1137
|
+
Now check supertyping relationships:
|
|
755
1138
|
|
|
756
1139
|
```scala
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
TypeId.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
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
|
|
779
|
-
TypeId
|
|
1172
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
1173
|
+
def isEquivalentTo(other: TypeId[?]): Boolean
|
|
1174
|
+
}
|
|
780
1175
|
```
|
|
781
1176
|
|
|
782
|
-
|
|
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.
|
|
1180
|
+
import zio.blocks.typeid._
|
|
788
1181
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
795
|
-
val reflect = Schema[Person].reflect
|
|
796
|
-
val typeId = reflect.typeId
|
|
1193
|
+
Now check type equivalence:
|
|
797
1194
|
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
1217
|
+
Type aliases normalize to the same type, making them equivalent:
|
|
1218
|
+
|
|
1219
|
+
```scala
|
|
1220
|
+
import zio.blocks.typeid._
|
|
803
1221
|
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1266
|
+
Parents are flattened across the full hierarchy:
|
|
819
1267
|
|
|
820
1268
|
```scala
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
1278
|
+
### Metadata
|
|
1279
|
+
|
|
1280
|
+
Methods for accessing annotations, self-type, alias target, and opaque representation.
|
|
839
1281
|
|
|
840
|
-
|
|
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
|
-
|
|
1287
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
1288
|
+
def annotations: List[Annotation]
|
|
1289
|
+
}
|
|
1290
|
+
```
|
|
844
1291
|
|
|
845
|
-
|
|
846
|
-
|
|
1292
|
+
```scala
|
|
1293
|
+
import zio.blocks.typeid._
|
|
847
1294
|
|
|
848
|
-
|
|
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
|
-
|
|
1301
|
+
A type with annotations reports each annotation by name; an unannotated type returns an empty list:
|
|
852
1302
|
|
|
853
1303
|
```scala
|
|
854
|
-
|
|
855
|
-
|
|
1304
|
+
// LegacyData has two annotations
|
|
1305
|
+
TypeId.of[LegacyData].annotations.map(_.name)
|
|
1306
|
+
// res153: List[String] = List("transient", "deprecated")
|
|
856
1307
|
|
|
857
|
-
//
|
|
1308
|
+
// Plain has no annotations
|
|
1309
|
+
TypeId.of[Plain].annotations
|
|
1310
|
+
// res154: List[Annotation] = List()
|
|
858
1311
|
```
|
|
859
1312
|
|
|
860
|
-
|
|
1313
|
+
#### `selfType` — Self-Type Annotation
|
|
861
1314
|
|
|
862
|
-
|
|
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
|
-
|
|
866
|
-
|
|
1318
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
1319
|
+
def selfType: Option[TypeRepr]
|
|
1320
|
+
}
|
|
1321
|
+
```
|
|
867
1322
|
|
|
868
|
-
|
|
1323
|
+
```scala
|
|
1324
|
+
import zio.blocks.typeid._
|
|
869
1325
|
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
881
|
-
|
|
1333
|
+
// Service declares a self-type dependency on Logger
|
|
1334
|
+
TypeId.of[Service].selfType
|
|
1335
|
+
// res156: Option[TypeRepr] = Some(Logger & Service)
|
|
882
1336
|
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1346
|
+
```scala
|
|
1347
|
+
sealed trait TypeId[A <: AnyKind] {
|
|
1348
|
+
def aliasedTo: Option[TypeRepr]
|
|
1349
|
+
}
|
|
1350
|
+
```
|
|
893
1351
|
|
|
894
1352
|
```scala
|
|
895
|
-
|
|
896
|
-
|
|
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](./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 util.ShowExpr.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 util.ShowExpr.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 util.ShowExpr.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
|
+
println("userIdAlias.name")
|
|
2967
|
+
show(userIdAlias.name)
|
|
2968
|
+
|
|
2969
|
+
println("userIdAlias.fullName")
|
|
2970
|
+
show(userIdAlias.fullName)
|
|
2971
|
+
|
|
2972
|
+
println("\n--- Normalization ---\n")
|
|
2973
|
+
|
|
2974
|
+
// Normalize the alias to its underlying type
|
|
2975
|
+
val normalized = TypeId.normalize(userIdAlias)
|
|
2976
|
+
|
|
2977
|
+
println("TypeId.normalize(userIdAlias).name")
|
|
2978
|
+
show(normalized.name)
|
|
2979
|
+
|
|
2980
|
+
println("TypeId.normalize(userIdAlias).fullName")
|
|
2981
|
+
show(normalized.fullName)
|
|
2982
|
+
|
|
2983
|
+
println("\n--- Equality with Normalization ---\n")
|
|
2984
|
+
|
|
2985
|
+
// Structural equality (not considering aliases)
|
|
2986
|
+
val anotherUserIdAlias = TypeId.alias[UserId](
|
|
2987
|
+
name = "UserId",
|
|
2988
|
+
owner = Owner.Root,
|
|
2989
|
+
typeParams = Nil,
|
|
2990
|
+
aliased = TypeRepr.Ref(TypeId.long)
|
|
2991
|
+
)
|
|
2992
|
+
|
|
2993
|
+
println("userIdAlias == anotherUserIdAlias")
|
|
2994
|
+
show(userIdAlias == anotherUserIdAlias)
|
|
2995
|
+
|
|
2996
|
+
println("TypeId.normalize(userIdAlias) == TypeId.long")
|
|
2997
|
+
show(TypeId.normalize(userIdAlias) == TypeId.long)
|
|
2998
|
+
|
|
2999
|
+
println("\n--- Erased TypeIds for Registries ---\n")
|
|
3000
|
+
|
|
3001
|
+
// Erased TypeIds are useful for type-indexed maps
|
|
3002
|
+
val intErased: TypeId.Erased = TypeId.int.erased
|
|
3003
|
+
val stringErased: TypeId.Erased = TypeId.string.erased
|
|
3004
|
+
val listIntErased: TypeId.Erased = TypeId.of[List[Int]].erased
|
|
3005
|
+
|
|
3006
|
+
println("TypeId.int.erased")
|
|
3007
|
+
show(intErased)
|
|
3008
|
+
|
|
3009
|
+
println("TypeId.string.erased")
|
|
3010
|
+
show(stringErased)
|
|
3011
|
+
|
|
3012
|
+
println("TypeId.of[List[Int]].erased")
|
|
3013
|
+
show(listIntErased)
|
|
3014
|
+
|
|
3015
|
+
println("\n--- Type Registry Using Erased TypeIds ---\n")
|
|
3016
|
+
|
|
3017
|
+
// Build a type registry
|
|
3018
|
+
val registry: Map[TypeId.Erased, String] = Map(
|
|
3019
|
+
TypeId.int.erased -> "Integer type",
|
|
3020
|
+
TypeId.string.erased -> "String type",
|
|
3021
|
+
TypeId.long.erased -> "Long type",
|
|
3022
|
+
TypeId.of[List[Int]].erased -> "List of integers"
|
|
3023
|
+
)
|
|
3024
|
+
|
|
3025
|
+
println("registry.get(TypeId.int.erased)")
|
|
3026
|
+
show(registry.get(TypeId.int.erased))
|
|
3027
|
+
|
|
3028
|
+
println("registry.get(TypeId.string.erased)")
|
|
3029
|
+
show(registry.get(TypeId.string.erased))
|
|
3030
|
+
|
|
3031
|
+
println("registry.get(TypeId.of[List[Int]].erased)")
|
|
3032
|
+
show(registry.get(TypeId.of[List[Int]].erased))
|
|
3033
|
+
|
|
3034
|
+
println("\n--- Querying the Registry ---\n")
|
|
3035
|
+
|
|
3036
|
+
// Look up types in the registry
|
|
3037
|
+
val intType = TypeId.of[Int].erased
|
|
3038
|
+
val doubleType = TypeId.of[Double].erased
|
|
3039
|
+
|
|
3040
|
+
println("registry.get(intType)")
|
|
3041
|
+
show(registry.get(intType))
|
|
3042
|
+
|
|
3043
|
+
println("registry.get(doubleType)")
|
|
3044
|
+
show(registry.get(doubleType))
|
|
3045
|
+
|
|
3046
|
+
println("\n═══════════════════════════════════════════════════════════════")
|
|
3047
|
+
}
|
|
3048
|
+
```
|
|
3049
|
+
|
|
3050
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/TypeIdNormalizationExample.scala))
|
|
3051
|
+
|
|
3052
|
+
```bash
|
|
3053
|
+
sbt "schema-examples/runMain typeid.TypeIdNormalizationExample"
|
|
3054
|
+
```
|
|
3055
|
+
|
|
3056
|
+
### Opaque Types
|
|
3057
|
+
|
|
3058
|
+
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:
|
|
3059
|
+
|
|
3060
|
+
```scala title="schema-examples/src/main/scala/typeid/OpaqueTypesExample.scala"
|
|
3061
|
+
/*
|
|
3062
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
3063
|
+
*
|
|
3064
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
3065
|
+
* you may not use this file except in compliance with the License.
|
|
3066
|
+
* You may obtain a copy of the License at
|
|
3067
|
+
*
|
|
3068
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
3069
|
+
*
|
|
3070
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
3071
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
3072
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
3073
|
+
* See the License for the specific language governing permissions and
|
|
3074
|
+
* limitations under the License.
|
|
3075
|
+
*/
|
|
3076
|
+
|
|
3077
|
+
package typeid
|
|
3078
|
+
|
|
3079
|
+
import zio.blocks.typeid._
|
|
3080
|
+
import util.ShowExpr.show
|
|
3081
|
+
|
|
3082
|
+
/**
|
|
3083
|
+
* Opaque Types Example
|
|
3084
|
+
*
|
|
3085
|
+
* Demonstrates how TypeId preserves the semantic distinction of opaque types,
|
|
3086
|
+
* enabling runtime type safety that pure Scala reflection cannot provide.
|
|
3087
|
+
*
|
|
3088
|
+
* Opaque types allow you to create distinct types that share the same
|
|
3089
|
+
* representation at runtime. TypeId captures this distinction, making it
|
|
3090
|
+
* possible to:
|
|
3091
|
+
* - Distinguish between different opaque types wrapping the same base type
|
|
3092
|
+
* - Build type-indexed registries that respect opaque type boundaries
|
|
3093
|
+
* - Implement runtime validators specific to each opaque type
|
|
3094
|
+
*
|
|
3095
|
+
* Run with: sbt "schema-examples/runMain typeid.OpaqueTypesExample"
|
|
3096
|
+
*/
|
|
3097
|
+
|
|
3098
|
+
object OpaqueTypesExample extends App {
|
|
3099
|
+
|
|
3100
|
+
println("═══════════════════════════════════════════════════════════════")
|
|
3101
|
+
println("Opaque Types with TypeId")
|
|
3102
|
+
println("═══════════════════════════════════════════════════════════════\n")
|
|
3103
|
+
|
|
3104
|
+
// Define domain-specific opaque types wrapping String
|
|
3105
|
+
// In a real application, these would enforce different validation rules
|
|
3106
|
+
opaque type UserId = String
|
|
3107
|
+
opaque type Email = String
|
|
3108
|
+
opaque type SessionToken = String
|
|
3109
|
+
|
|
3110
|
+
println("--- Opaque Type Definitions ---\n")
|
|
3111
|
+
println("opaque type UserId = String")
|
|
3112
|
+
println("opaque type Email = String")
|
|
3113
|
+
println("opaque type SessionToken = String\n")
|
|
3114
|
+
|
|
3115
|
+
// Derive TypeIds for each opaque type
|
|
3116
|
+
val userIdType = TypeId.of[UserId]
|
|
3117
|
+
val emailType = TypeId.of[Email]
|
|
3118
|
+
val sessionTokenType = TypeId.of[SessionToken]
|
|
3119
|
+
val stringType = TypeId.string
|
|
3120
|
+
|
|
3121
|
+
println("--- TypeId Derivation ---\n")
|
|
3122
|
+
|
|
3123
|
+
println("TypeId.of[UserId].name")
|
|
3124
|
+
show(userIdType.name)
|
|
3125
|
+
|
|
3126
|
+
println("TypeId.of[Email].name")
|
|
3127
|
+
show(emailType.name)
|
|
3128
|
+
|
|
3129
|
+
println("TypeId.of[SessionToken].name")
|
|
3130
|
+
show(sessionTokenType.name)
|
|
3131
|
+
|
|
3132
|
+
println("\n--- Opaque Types vs Base Type ---\n")
|
|
3133
|
+
|
|
3134
|
+
// Key insight: opaque types are distinct from their representation type
|
|
3135
|
+
println("TypeId.of[UserId].isEquivalentTo(TypeId.string)")
|
|
3136
|
+
show(userIdType.isEquivalentTo(stringType))
|
|
3137
|
+
|
|
3138
|
+
println("TypeId.of[Email].isEquivalentTo(TypeId.string)")
|
|
3139
|
+
show(emailType.isEquivalentTo(stringType))
|
|
3140
|
+
|
|
3141
|
+
println("TypeId.of[SessionToken].isEquivalentTo(TypeId.string)")
|
|
3142
|
+
show(sessionTokenType.isEquivalentTo(stringType))
|
|
3143
|
+
|
|
3144
|
+
println("\n--- Opaque Types are Distinct from Each Other ---\n")
|
|
3145
|
+
|
|
3146
|
+
println("TypeId.of[UserId].isEquivalentTo(TypeId.of[Email])")
|
|
3147
|
+
show(userIdType.isEquivalentTo(emailType))
|
|
3148
|
+
|
|
3149
|
+
println("TypeId.of[Email].isEquivalentTo(TypeId.of[SessionToken])")
|
|
3150
|
+
show(emailType.isEquivalentTo(sessionTokenType))
|
|
3151
|
+
|
|
3152
|
+
println("TypeId.of[UserId].isEquivalentTo(TypeId.of[SessionToken])")
|
|
3153
|
+
show(userIdType.isEquivalentTo(sessionTokenType))
|
|
3154
|
+
|
|
3155
|
+
println("\n--- Real-World Use Case: Type-Safe Registry ---\n")
|
|
3156
|
+
|
|
3157
|
+
// Define validators for each opaque type
|
|
3158
|
+
trait Validator {
|
|
3159
|
+
def validate(value: String): Boolean
|
|
3160
|
+
def errorMessage: String
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
val userIdValidator = new Validator {
|
|
3164
|
+
def validate(value: String): Boolean = value.nonEmpty && value.forall(_.isDigit)
|
|
3165
|
+
def errorMessage = "UserId must be non-empty digits"
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
val emailValidator = new Validator {
|
|
3169
|
+
def validate(value: String): Boolean = value.contains("@") && value.contains(".")
|
|
3170
|
+
def errorMessage = "Email must contain @ and ."
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
val sessionTokenValidator = new Validator {
|
|
3174
|
+
def validate(value: String): Boolean = value.length >= 32
|
|
3175
|
+
def errorMessage = "SessionToken must be at least 32 characters"
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
// Build a type-indexed registry of validators
|
|
3179
|
+
// This demonstrates the power of TypeId: we can safely dispatch
|
|
3180
|
+
// to different validators based on opaque type identity
|
|
3181
|
+
val validatorRegistry: Map[TypeId.Erased, Validator] = Map(
|
|
3182
|
+
TypeId.of[UserId].erased -> userIdValidator,
|
|
3183
|
+
TypeId.of[Email].erased -> emailValidator,
|
|
3184
|
+
TypeId.of[SessionToken].erased -> sessionTokenValidator
|
|
3185
|
+
)
|
|
3186
|
+
|
|
3187
|
+
println("Built validator registry keyed by opaque type")
|
|
3188
|
+
println("Validators can enforce different validation rules per type\n")
|
|
3189
|
+
|
|
3190
|
+
// Demonstrate validation dispatch
|
|
3191
|
+
def validateString(value: String, typeId: TypeId[_]): Boolean =
|
|
3192
|
+
validatorRegistry
|
|
3193
|
+
.get(typeId.erased)
|
|
3194
|
+
.map(_.validate(value))
|
|
3195
|
+
.getOrElse {
|
|
3196
|
+
println(s"No validator found for type: ${typeId.fullName}")
|
|
3197
|
+
false
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
def getValidationError(typeId: TypeId[_]): String =
|
|
3201
|
+
validatorRegistry
|
|
3202
|
+
.get(typeId.erased)
|
|
3203
|
+
.map(_.errorMessage)
|
|
3204
|
+
.getOrElse("Unknown validator")
|
|
3205
|
+
|
|
3206
|
+
println("--- Validation Examples ---\n")
|
|
3207
|
+
|
|
3208
|
+
val testUserId = "12345"
|
|
3209
|
+
val testEmail = "user@example.com"
|
|
3210
|
+
val testToken = "a" * 32
|
|
3211
|
+
|
|
3212
|
+
println(s"Validating UserId: '$testUserId'")
|
|
3213
|
+
if (validateString(testUserId, TypeId.of[UserId])) {
|
|
3214
|
+
println("✓ Valid UserId\n")
|
|
3215
|
+
} else {
|
|
3216
|
+
println(s"✗ Invalid: ${getValidationError(TypeId.of[UserId])}\n")
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
println(s"Validating Email: '$testEmail'")
|
|
3220
|
+
if (validateString(testEmail, TypeId.of[Email])) {
|
|
3221
|
+
println("✓ Valid Email\n")
|
|
3222
|
+
} else {
|
|
3223
|
+
println(s"✗ Invalid: ${getValidationError(TypeId.of[Email])}\n")
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
println(s"Validating SessionToken: '$testToken'")
|
|
3227
|
+
if (validateString(testToken, TypeId.of[SessionToken])) {
|
|
3228
|
+
println("✓ Valid SessionToken\n")
|
|
3229
|
+
} else {
|
|
3230
|
+
println(s"✗ Invalid: ${getValidationError(TypeId.of[SessionToken])}\n")
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
println("--- What Pure Scala Cannot Do ---\n")
|
|
3234
|
+
|
|
3235
|
+
println("With pure Scala reflection:")
|
|
3236
|
+
println("- classOf[UserId] == classOf[String] (erased at runtime)")
|
|
3237
|
+
println("- classOf[Email] == classOf[String] (erased at runtime)")
|
|
3238
|
+
println("- You cannot distinguish opaque types from their base type\n")
|
|
3239
|
+
|
|
3240
|
+
println("With TypeId:")
|
|
3241
|
+
println("- TypeId.of[UserId] != TypeId.of[String] (preserved)")
|
|
3242
|
+
println("- TypeId.of[Email] != TypeId.of[String] (preserved)")
|
|
3243
|
+
println("- You can build type-safe validators and registries\n")
|
|
3244
|
+
|
|
3245
|
+
println("═══════════════════════════════════════════════════════════════")
|
|
3246
|
+
}
|
|
3247
|
+
```
|
|
3248
|
+
|
|
3249
|
+
([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/typeid/OpaqueTypesExample.scala))
|
|
897
3250
|
|
|
898
|
-
|
|
899
|
-
|
|
3251
|
+
```bash
|
|
3252
|
+
sbt "schema-examples/runMain typeid.OpaqueTypesExample"
|
|
900
3253
|
```
|