opencode-effect-enforcer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,975 @@
1
+ ---
2
+ name: effect-schema-composition
3
+ description: Master Effect Schema composition patterns including Schema.decodeTo, transformations, filters, and validation. Use this skill when working with complex schema compositions, multi-step transformations, or when you need to validate and transform data through multiple stages.
4
+ ---
5
+
6
+ # Schema Composition Skill
7
+
8
+ Expert guidance for composing, transforming, and validating data with Effect Schema (v4).
9
+
10
+ ## Effect Source Reference
11
+
12
+ The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
13
+ Browse and read files there directly to look up APIs, types, and implementations.
14
+
15
+ Reference this for:
16
+
17
+ - Full Schema API: `packages/effect/SCHEMA.md`
18
+ - Schema source: `packages/effect/src/Schema.ts`
19
+ - SchemaTransformation source: `packages/effect/src/SchemaTransformation.ts`
20
+ - Migration guide: `MIGRATION.md`
21
+ - Effect source: `packages/effect/src/`
22
+
23
+ ## Core Concepts
24
+
25
+ ### The Schema Type
26
+
27
+ Every schema in Effect has the type signature `Schema<Type, Encoded, Context>` where:
28
+
29
+ - **Type**: The validated, decoded output type (what you get after successful decoding)
30
+ - **Encoded**: The raw input type (what you provide for decoding)
31
+ - **Context**: External dependencies required for encoding/decoding (often `never`)
32
+
33
+ **Example:**
34
+
35
+ ```typescript
36
+ import { Schema } from 'effect';
37
+
38
+ // Schema<number, string, never>
39
+ // ^Type ^Encoded ^Context
40
+ const NumberFromString = Schema.NumberFromString;
41
+ ```
42
+
43
+ ### Decoding vs Encoding
44
+
45
+ - **Decoding**: Transform `Encoded` → `Type` (e.g., string "123" → number 123)
46
+ - **Encoding**: Transform `Type` → `Encoded` (e.g., number 123 → string "123")
47
+
48
+ Effect Schema follows "parse, don't validate" — schemas transform data into the desired format, not just check validity.
49
+
50
+ ## Schema.decodeTo — Chaining Transformations
51
+
52
+ Use `Schema.decodeTo` to chain schemas with **different types** at each stage. It connects the output type of one schema to the input type of another. This replaces the v3 `Schema.compose`.
53
+
54
+ **When to Use:**
55
+
56
+ - Multi-step transformations where each stage changes the type
57
+ - Connecting parsing and validation steps
58
+ - Building pipelines from `Encoded → Intermediate → Type`
59
+
60
+ **Example — Schema composition (no transformation):**
61
+
62
+ ```typescript
63
+ import { Schema, SchemaTransformation } from 'effect';
64
+
65
+ // Convert meters → kilometers → miles via schema composition
66
+ const KilometersFromMeters = Schema.Finite.pipe(
67
+ Schema.decode(
68
+ SchemaTransformation.transform({
69
+ decode: (meters) => meters / 1000,
70
+ encode: (kilometers) => kilometers * 1000
71
+ })
72
+ )
73
+ );
74
+
75
+ const MilesFromKilometers = Schema.Finite.pipe(
76
+ Schema.decode(
77
+ SchemaTransformation.transform({
78
+ decode: (kilometers) => kilometers * 0.621371,
79
+ encode: (miles) => miles / 0.621371
80
+ })
81
+ )
82
+ );
83
+
84
+ // Compose the two schemas — no explicit transformation needed
85
+ const MilesFromMeters = KilometersFromMeters.pipe(
86
+ Schema.decodeTo(MilesFromKilometers)
87
+ );
88
+ ```
89
+
90
+ **Example — Boolean from String via Literal:**
91
+
92
+ ```typescript
93
+ import { Schema, SchemaTransformation } from 'effect';
94
+
95
+ const BooleanFromString = Schema.Literals(['on', 'off']).pipe(
96
+ Schema.decodeTo(
97
+ Schema.Boolean,
98
+ SchemaTransformation.transform({
99
+ decode: (literal) => literal === 'on',
100
+ encode: (bool) => (bool ? 'on' : 'off')
101
+ })
102
+ )
103
+ );
104
+ ```
105
+
106
+ ### Schema.pipe with .check() — Sequential Refinements
107
+
108
+ Use `.check()` to apply **filters and refinements** to the same type. It doesn't change the type, just adds validation constraints.
109
+
110
+ **When to Use:**
111
+
112
+ - Adding validation rules to an existing schema
113
+ - Chaining multiple filters on the same type
114
+ - Refining without transformation
115
+
116
+ **Example — Number Validation:**
117
+
118
+ ```typescript
119
+ import { Schema } from 'effect';
120
+
121
+ const PositiveInt = Schema.Number.check(
122
+ Schema.isInt(),
123
+ Schema.isGreaterThan(0)
124
+ );
125
+
126
+ // Type: Schema<number, number, never>
127
+ // Both Type and Encoded are `number`
128
+ ```
129
+
130
+ **Example — String Validation:**
131
+
132
+ ```typescript
133
+ import { Schema } from 'effect';
134
+
135
+ const ValidEmail = Schema.String.check(
136
+ Schema.isTrimmed(),
137
+ Schema.isLowercased(),
138
+ Schema.isMinLength(5),
139
+ Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
140
+ );
141
+ ```
142
+
143
+ ### Key Differences
144
+
145
+ | Aspect | Schema.decodeTo | .check() |
146
+ | --------------- | -------------------------- | -------------------------- |
147
+ | **Purpose** | Chain transformations | Apply refinements |
148
+ | **Type Change** | Changes type at each stage | Type stays the same |
149
+ | **Example** | `string → number` | `number → positive number` |
150
+ | **Use Case** | Multi-step parsing | Validation constraints |
151
+
152
+ ## Built-in Filters (Checks)
153
+
154
+ Filters add validation constraints without changing the schema's type. Apply them with `.check()`.
155
+
156
+ ### String Filters
157
+
158
+ ```typescript
159
+ import { Schema } from 'effect';
160
+
161
+ // Length constraints
162
+ Schema.String.check(Schema.isMaxLength(5));
163
+ Schema.String.check(Schema.isMinLength(5));
164
+ Schema.String.check(Schema.isNonEmpty()); // non-empty string
165
+ Schema.String.check(Schema.isLengthBetween(2, 4));
166
+
167
+ // Pattern matching
168
+ Schema.String.check(Schema.isPattern(/^[a-z]+$/));
169
+ Schema.String.check(Schema.isStartsWith('prefix'));
170
+ Schema.String.check(Schema.isEndsWith('suffix'));
171
+ Schema.String.check(Schema.isIncludes('substring'));
172
+
173
+ // Case and whitespace validation
174
+ Schema.String.check(Schema.isTrimmed()); // No leading/trailing whitespace
175
+ Schema.String.check(Schema.isLowercased()); // All lowercase
176
+ Schema.String.check(Schema.isUppercased()); // All uppercase
177
+ Schema.String.check(Schema.isCapitalized()); // First letter capitalized
178
+
179
+ // String formats
180
+ Schema.String.check(Schema.isUUID());
181
+ Schema.String.check(Schema.isULID());
182
+ Schema.String.check(Schema.isBase64());
183
+ Schema.String.check(Schema.isBase64Url());
184
+ ```
185
+
186
+ ### Number Filters
187
+
188
+ ```typescript
189
+ import { Schema } from 'effect';
190
+
191
+ // Range constraints
192
+ Schema.Number.check(Schema.isGreaterThan(5));
193
+ Schema.Number.check(Schema.isGreaterThanOrEqualTo(5));
194
+ Schema.Number.check(Schema.isLessThan(5));
195
+ Schema.Number.check(Schema.isLessThanOrEqualTo(5));
196
+ Schema.Number.check(Schema.isBetween({ minimum: -2, maximum: 2 }));
197
+
198
+ // Type constraints
199
+ Schema.Number.check(Schema.isInt()); // integer
200
+ Schema.Number.check(Schema.isInt32()); // 32-bit integer
201
+ Schema.Number.check(Schema.isFinite()); // not Infinity/NaN
202
+ Schema.Number.check(Schema.isMultipleOf(5));
203
+ Schema.Natural; // canonical non-negative safe integer
204
+
205
+ // Sign constraints (use comparison filters)
206
+ Schema.Number.check(Schema.isGreaterThan(0)); // positive (> 0)
207
+ Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)); // non-negative (>= 0)
208
+ Schema.Number.check(Schema.isLessThan(0)); // negative (< 0)
209
+ Schema.Number.check(Schema.isLessThanOrEqualTo(0)); // non-positive (<= 0)
210
+ ```
211
+
212
+ Prefer `Schema.Natural` over a hand-built combination of integer, safe-integer, and non-negative checks when that is the domain invariant.
213
+
214
+ ### Array Filters
215
+
216
+ ```typescript
217
+ import { Schema } from 'effect';
218
+
219
+ Schema.Array(Schema.Number).check(Schema.isMinLength(2));
220
+ Schema.Array(Schema.Number).check(Schema.isMaxLength(5));
221
+ Schema.Array(Schema.Number).check(Schema.isLengthBetween(2, 5));
222
+ ```
223
+
224
+ ### Combining Multiple Filters
225
+
226
+ Pass multiple filters to a single `.check()` call:
227
+
228
+ ```typescript
229
+ import { Schema } from 'effect';
230
+
231
+ const schema = Schema.String.check(Schema.isMinLength(3), Schema.isTrimmed());
232
+ ```
233
+
234
+ With `{ errors: "all" }`, all filters are evaluated and multiple issues can be reported at once.
235
+
236
+ ## Custom Filters
237
+
238
+ Define custom validation logic using `Schema.makeFilter()`:
239
+
240
+ ```typescript
241
+ import { Schema } from 'effect';
242
+
243
+ const LongString = Schema.String.check(
244
+ Schema.makeFilter(
245
+ (s) => s.length >= 10 || 'a string at least 10 characters long'
246
+ )
247
+ );
248
+ ```
249
+
250
+ ### Filter Return Types
251
+
252
+ The filter predicate can return:
253
+
254
+ | Return Type | Meaning |
255
+ | -------------------------------------- | -------------------------------------------- |
256
+ | `true` or `undefined` | Validation passes |
257
+ | `false` | Validation fails (no error message) |
258
+ | `string` | Validation fails with error message |
259
+ | `SchemaIssue.Issue` | Validation fails with a structured issue |
260
+ | `{ path, issue }` | Validation fails at a nested path |
261
+ | `ReadonlyArray<Schema.FilterIssue>` | Reports multiple filter issues together |
262
+
263
+ ### Filter Annotations
264
+
265
+ Add metadata to filters for better error messages:
266
+
267
+ ```typescript
268
+ import { Schema } from 'effect';
269
+
270
+ const LongString = Schema.String.check(
271
+ Schema.makeFilter(
272
+ (s) => s.length >= 10 || 'a string at least 10 characters long',
273
+ {
274
+ title: 'LongString',
275
+ description: 'A string with at least 10 characters'
276
+ }
277
+ )
278
+ );
279
+ ```
280
+
281
+ ### Filter Groups
282
+
283
+ Group filters into a reusable unit with `Schema.makeFilterGroup`:
284
+
285
+ ```typescript
286
+ import { Schema } from 'effect';
287
+
288
+ const isInt32 = Schema.makeFilterGroup(
289
+ [
290
+ Schema.isInt(),
291
+ Schema.isBetween({ minimum: -2147483648, maximum: 2147483647 })
292
+ ],
293
+ {
294
+ title: 'isInt32',
295
+ description: 'a 32-bit integer'
296
+ }
297
+ );
298
+
299
+ Schema.Number.check(isInt32);
300
+ ```
301
+
302
+ ### Error Paths for Form Validation
303
+
304
+ Associate errors with specific fields using `path` in `makeFilter`:
305
+
306
+ ```typescript
307
+ import { Schema } from 'effect';
308
+
309
+ const Password = Schema.Trimmed.check(Schema.isMinLength(2));
310
+
311
+ const MyForm = Schema.Struct({
312
+ password: Password,
313
+ confirm_password: Password
314
+ }).check(
315
+ Schema.makeFilter((input) => {
316
+ if (input.password !== input.confirm_password) {
317
+ return {
318
+ path: ['confirm_password'],
319
+ issue: 'Passwords do not match'
320
+ };
321
+ }
322
+ })
323
+ );
324
+ ```
325
+
326
+ ### Effectful Filters
327
+
328
+ Use `SchemaGetter.checkEffect` for async validation inside a `Schema.decode` transformation:
329
+
330
+ ```typescript
331
+ import {
332
+ Effect,
333
+ Option,
334
+ Result,
335
+ Schema,
336
+ SchemaGetter,
337
+ SchemaIssue
338
+ } from 'effect';
339
+
340
+ async function validateUsername(username: string) {
341
+ return Promise.resolve(username === 'gcanti');
342
+ }
343
+
344
+ const ValidUsername = Schema.String.pipe(
345
+ Schema.decode({
346
+ decode: SchemaGetter.checkEffect((username) =>
347
+ Effect.promise(() =>
348
+ validateUsername(username).then((valid) =>
349
+ valid
350
+ ? undefined
351
+ : new SchemaIssue.InvalidValue(Option.some(username), {
352
+ title: 'Invalid username'
353
+ })
354
+ )
355
+ )
356
+ ),
357
+ encode: SchemaGetter.passthrough()
358
+ })
359
+ );
360
+ ```
361
+
362
+ ## Built-in Transformations
363
+
364
+ Transformations are first-class reusable objects in v4. Apply them with `Schema.decode` (same source/target type) or `Schema.decodeTo` (different types).
365
+
366
+ ### JSON String Transformations
367
+
368
+ `Schema.fromJsonString(schema, options)` accepts a `JSON.parse` `reviver` for decoding and `replacer` / `space` options for encoding. Because a reviver may produce arbitrary values, the supplied schema remains responsible for validating the revived result.
369
+
370
+ ```typescript
371
+ import { Schema } from 'effect';
372
+
373
+ const PayloadJson = Schema.fromJsonString(
374
+ Schema.Struct({ value: Schema.String }),
375
+ {
376
+ reviver: (key, value) => key === 'value' ? 'revived' : value,
377
+ space: 2
378
+ }
379
+ );
380
+ ```
381
+
382
+ ### String Transformations
383
+
384
+ ```typescript
385
+ import { Schema, SchemaTransformation } from 'effect';
386
+
387
+ // Whitespace and case transformations (applied with Schema.decode)
388
+ Schema.String.pipe(Schema.decode(SchemaTransformation.trim()));
389
+ Schema.String.pipe(Schema.decode(SchemaTransformation.toLowerCase()));
390
+ Schema.String.pipe(Schema.decode(SchemaTransformation.toUpperCase()));
391
+
392
+ // Capitalize / Uncapitalize require decodeTo with a checked target
393
+ Schema.String.pipe(
394
+ Schema.decodeTo(
395
+ Schema.String.check(Schema.isCapitalized()),
396
+ SchemaTransformation.capitalize()
397
+ )
398
+ );
399
+ Schema.String.pipe(
400
+ Schema.decodeTo(
401
+ Schema.String.check(Schema.isLowercased()),
402
+ SchemaTransformation.toLowerCase()
403
+ )
404
+ );
405
+
406
+ // Pre-built transformation schemas
407
+ Schema.Trimmed; // Schema<string, string> — trimmed string
408
+ Schema.NonEmptyString; // Schema<string, string> — non-empty
409
+ ```
410
+
411
+ ### Number Transformations
412
+
413
+ ```typescript
414
+ import { Schema, SchemaTransformation } from 'effect';
415
+
416
+ // Parse numbers from strings (built-in)
417
+ Schema.NumberFromString; // "123" → 123
418
+ Schema.FiniteFromString; // "123" → 123 (finite only)
419
+
420
+ // Custom inline
421
+ Schema.Finite.pipe(
422
+ Schema.decode(
423
+ SchemaTransformation.transform({
424
+ decode: (meters) => meters / 1000,
425
+ encode: (km) => km * 1000
426
+ })
427
+ )
428
+ );
429
+ ```
430
+
431
+ ### Duration Transformations
432
+
433
+ ```typescript
434
+ import { Schema, SchemaTransformation } from 'effect';
435
+
436
+ // Built-in duration parsing, including "Infinity" and "-Infinity"
437
+ Schema.DurationFromString; // "1 second" → Duration.Duration
438
+
439
+ const DurationFromString = Schema.String.pipe(
440
+ Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromString)
441
+ );
442
+ ```
443
+
444
+ ### Split (manual implementation)
445
+
446
+ `Schema.split` was removed in v4. Implement it manually:
447
+
448
+ ```typescript
449
+ import { Schema, SchemaTransformation } from 'effect';
450
+
451
+ function split(separator: string) {
452
+ return Schema.String.pipe(
453
+ Schema.decodeTo(
454
+ Schema.Array(Schema.String),
455
+ SchemaTransformation.transform({
456
+ decode: (s) => s.split(separator) as ReadonlyArray<string>,
457
+ encode: (as) => as.join(separator)
458
+ })
459
+ )
460
+ );
461
+ }
462
+ ```
463
+
464
+ ## Custom Transformations
465
+
466
+ ### SchemaTransformation.transform — Simple Transformations
467
+
468
+ Use `SchemaTransformation.transform` when the transformation always succeeds:
469
+
470
+ ```typescript
471
+ import { Schema, SchemaTransformation } from 'effect';
472
+
473
+ const BooleanFromString = Schema.Literals(['on', 'off']).pipe(
474
+ Schema.decodeTo(
475
+ Schema.Boolean,
476
+ SchemaTransformation.transform({
477
+ decode: (literal) => literal === 'on',
478
+ encode: (bool) => (bool ? 'on' : 'off')
479
+ })
480
+ )
481
+ );
482
+ ```
483
+
484
+ ### SchemaTransformation.transformOrFail — Transformations That Can Fail
485
+
486
+ Use `SchemaTransformation.transformOrFail` when transformation might fail:
487
+
488
+ ```typescript
489
+ import {
490
+ Effect,
491
+ Number,
492
+ Option,
493
+ Schema,
494
+ SchemaGetter,
495
+ SchemaIssue
496
+ } from 'effect';
497
+
498
+ const NumberFromString = Schema.String.pipe(
499
+ Schema.decodeTo(Schema.Number, {
500
+ decode: SchemaGetter.transformOrFail((s) =>
501
+ Option.match(Number.parse(s), {
502
+ onNone: () =>
503
+ Effect.fail(
504
+ new SchemaIssue.InvalidValue(Option.some(s))
505
+ ),
506
+ onSome: (n) => Effect.succeed(n)
507
+ })
508
+ ),
509
+ encode: SchemaGetter.String()
510
+ })
511
+ );
512
+ ```
513
+
514
+ ### SchemaTransformation.transformOptional — Optional Key Transforms
515
+
516
+ Use `SchemaTransformation.transformOptional` for optional key transformations:
517
+
518
+ ```typescript
519
+ import { Option, Schema, SchemaTransformation } from 'effect';
520
+
521
+ const OptionFromNonEmptyString = Schema.optionalKey(Schema.String).pipe(
522
+ Schema.decodeTo(
523
+ Schema.Option(Schema.NonEmptyString),
524
+ SchemaTransformation.transformOptional({
525
+ decode: (oe) =>
526
+ Option.isSome(oe) && oe.value !== ''
527
+ ? Option.some(Option.some(oe.value))
528
+ : Option.some(Option.none()),
529
+ encode: (ot) => Option.flatten(ot)
530
+ })
531
+ )
532
+ );
533
+ ```
534
+
535
+ ## Streamlined Effect Patterns
536
+
537
+ ### Direct flatMap with Schema.decodeUnknownEffect
538
+
539
+ `Schema.decodeUnknownEffect(schema)` returns a function that can be passed directly to `Effect.flatMap`:
540
+
541
+ ```typescript
542
+ import { Effect, Schema } from 'effect';
543
+
544
+ declare const self: Effect.Effect<unknown, unknown, unknown>;
545
+ declare const schema: Schema.Schema<unknown, unknown, never>;
546
+ declare const toError: (e: unknown) => unknown;
547
+
548
+ // Streamlined
549
+ self.pipe(
550
+ Effect.flatMap(Schema.decodeUnknownEffect(schema)),
551
+ Effect.mapError(toError)
552
+ );
553
+ ```
554
+
555
+ ### Extract Schema Factories
556
+
557
+ Create reusable schema factories for common patterns:
558
+
559
+ ```typescript
560
+ import { Effect, Schema } from 'effect';
561
+
562
+ declare const toAssertionError: (e: unknown) => Error;
563
+
564
+ const createGreaterThanSchema = (n: number) =>
565
+ Schema.Number.check(Schema.isGreaterThan(n));
566
+
567
+ export const beGreaterThan =
568
+ (n: number) =>
569
+ <E, R>(self: Effect.Effect<number, E, R>) =>
570
+ self.pipe(
571
+ Effect.flatMap(
572
+ Schema.decodeUnknownEffect(createGreaterThanSchema(n))
573
+ ),
574
+ Effect.mapError(toAssertionError)
575
+ );
576
+ ```
577
+
578
+ ## Decoding and Encoding
579
+
580
+ ### Constructor vs Boundary Decoder
581
+
582
+ Keep decoded shapes schema-first with `Schema.Class`. Choose construction and decoding APIs by input trust and failure semantics:
583
+
584
+ | API | Use Case | Failure |
585
+ | --- | --- | --- |
586
+ | `schema.make` | Construct from typed constructor input; trusted data or abort-on-invalid paths | Throws on failed type-side checks |
587
+ | `schema.makeEffect` | Construct from typed constructor input inside Effect | `Effect` failure with `SchemaIssue.Issue` |
588
+ | `Schema.decodeUnknownEffect(schema)` | Default for unknown boundary input | `Effect` failure with `Schema.SchemaError` |
589
+ | `Schema.decodeUnknownSync(schema)` | Scripts, tests, or startup paths where throwing is acceptable | Throws `Schema.SchemaError` |
590
+ | `Schema.decodeUnknownOption(schema)` | Only when mismatch details are intentionally discarded | `Option.none()` for schema mismatches |
591
+ | `Schema.decodeUnknownResult(schema)` | Pure code needing explicit success or failure without Effect | `Result` failure with `Schema.SchemaError` |
592
+
593
+ `make` and `makeEffect` apply constructor defaults and type-side checks. They are constructors, not substitutes for decoding unknown external input.
594
+
595
+ ### Decoding APIs
596
+
597
+ | API | Return Type | Use Case |
598
+ | ---------------------- | ------------------------------------ | ------------------------------- |
599
+ | `decodeUnknownSync` | `Type` (throws on error) | Sync decoding, immediate error |
600
+ | `decodeUnknownOption` | `Option<Type>` | Sync decoding, no error details |
601
+ | `decodeUnknownResult` | `Result<Type, Schema.SchemaError>` | Pure, explicit success/failure |
602
+ | `decodeUnknownExit` | `Exit<Type, Schema.SchemaError>` | Sync decoding, error handling |
603
+ | `decodeUnknownPromise` | `Promise<Type>` | Async decoding |
604
+ | `decodeUnknownEffect` | `Effect<Type, Schema.SchemaError, Context>` | Full Effect-based decoding |
605
+
606
+ **Example:**
607
+
608
+ ```typescript
609
+ import { Schema } from 'effect';
610
+
611
+ const Person = Schema.Struct({
612
+ name: Schema.String,
613
+ age: Schema.Number
614
+ });
615
+
616
+ // Sync with error throwing
617
+ const person1 = Schema.decodeUnknownSync(Person)({ name: 'Alice', age: 30 });
618
+
619
+ // Sync with Exit
620
+ const result = Schema.decodeUnknownExit(Person)({ name: 'Alice', age: 30 });
621
+
622
+ // Effect-based (required for async schemas)
623
+ const asyncResult = Schema.decodeUnknownEffect(Person)({
624
+ name: 'Alice',
625
+ age: 30
626
+ });
627
+ ```
628
+
629
+ ### Encoding APIs
630
+
631
+ | API | Return Type | Use Case |
632
+ | ------------------- | --------------------------------------- | ------------------------------- |
633
+ | `encodeSync` | `Encoded` (throws on error) | Sync encoding, immediate error |
634
+ | `encodeOption` | `Option<Encoded>` | Sync encoding, no error details |
635
+ | `encodeUnknownExit` | `Exit<Encoded, Schema.SchemaError>` | Sync encoding, error handling |
636
+ | `encodePromise` | `Promise<Encoded>` | Async encoding |
637
+ | `encodeEffect` | `Effect<Encoded, Schema.SchemaError, Context>` | Full Effect-based encoding |
638
+
639
+ ## Struct and Object Schemas
640
+
641
+ ### Basic Struct
642
+
643
+ ```typescript
644
+ import { Schema } from 'effect';
645
+
646
+ const Person = Schema.Struct({
647
+ name: Schema.String,
648
+ age: Schema.Number
649
+ });
650
+
651
+ // Type: { readonly name: string; readonly age: number }
652
+ ```
653
+
654
+ ### Optional Fields
655
+
656
+ Optionality describes the encoded contract, not constructor convenience. Use `optionalKey` only when the key may be absent, `optional` only when explicit `undefined` is accepted, and nullish schemas only when those values are valid encoded inputs.
657
+
658
+ ```typescript
659
+ import { Schema } from 'effect';
660
+
661
+ const User = Schema.Struct({
662
+ username: Schema.String,
663
+ email: Schema.optional(Schema.String), // key?: string | undefined
664
+ bio: Schema.optionalKey(Schema.String) // key?: string (exact)
665
+ });
666
+ ```
667
+
668
+ ### Nullable Fields
669
+
670
+ ```typescript
671
+ import { Schema } from 'effect';
672
+
673
+ const Data = Schema.Struct({
674
+ value: Schema.NullOr(Schema.String)
675
+ });
676
+
677
+ // Type: { readonly value: string | null }
678
+ ```
679
+
680
+ ### Partial and Required (via mapFields)
681
+
682
+ ```typescript
683
+ import { Schema, Struct } from 'effect';
684
+
685
+ const User = Schema.Struct({
686
+ username: Schema.String,
687
+ email: Schema.optional(Schema.String)
688
+ });
689
+
690
+ // Make all fields optional (allows undefined)
691
+ const PartialUser = User.mapFields(Struct.map(Schema.optional));
692
+
693
+ // Make all fields optional (exact — key can be absent)
694
+ const ExactPartialUser = User.mapFields(Struct.map(Schema.optionalKey));
695
+
696
+ // Make all fields required
697
+ const RequiredUser = PartialUser.mapFields(Struct.map(Schema.requiredKey));
698
+ ```
699
+
700
+ ### Picking and Omitting (via mapFields)
701
+
702
+ ```typescript
703
+ import { Schema, Struct } from 'effect';
704
+
705
+ const Recipe = Schema.Struct({
706
+ id: Schema.String,
707
+ name: Schema.String,
708
+ ingredients: Schema.Array(Schema.String)
709
+ });
710
+
711
+ const JustTheName = Recipe.mapFields(Struct.pick(['name']));
712
+ const NoIDRecipe = Recipe.mapFields(Struct.omit(['id']));
713
+ ```
714
+
715
+ ### Extending Structs (via mapFields or fieldsAssign)
716
+
717
+ ```typescript
718
+ import { Schema, Struct } from 'effect';
719
+
720
+ const Dog = Schema.Struct({
721
+ name: Schema.String,
722
+ age: Schema.Number
723
+ });
724
+
725
+ // Method 1: Using mapFields + Struct.assign
726
+ const DogWithBreed = Dog.mapFields(Struct.assign({ breed: Schema.String }));
727
+
728
+ // Method 2: Using fieldsAssign (more succinct)
729
+ const DogWithBreed2 = Dog.pipe(Schema.fieldsAssign({ breed: Schema.String }));
730
+
731
+ // Method 3: Spreading fields (still works)
732
+ const DogWithBreed3 = Schema.Struct({
733
+ ...Dog.fields,
734
+ breed: Schema.String
735
+ });
736
+ ```
737
+
738
+ ### Semantic Contract Reuse
739
+
740
+ - Reuse `.fields`, `Schema.fieldsAssign(...)`, and `.mapFields(...)` only when the resulting contracts are genuinely related. Keep external and domain shapes as named `Schema.Class` models rather than building one oversized inheritance-by-schema object.
741
+ - Apply `Schema.encodeKeys({ decodedName: 'encoded_name' })` after assembling the full shape when wire or storage key names are the only difference. Keep an explicit boundary mapping when behavior, joins, validation, or domain translation differs.
742
+ - Use `Schema.extendTo(fields, derive)` sparingly for structural projections with decoded-only derived fields. Derived fields are removed during encoding; do not use it to hide a distinct domain contract or replace a schema class.
743
+
744
+ ## Advanced Composition Patterns
745
+
746
+ ### Combining Arrays and Transformations
747
+
748
+ ```typescript
749
+ import { Schema, SchemaTransformation } from 'effect';
750
+
751
+ const ReadonlySetFromArray = <A, I, R>(
752
+ itemSchema: Schema.Schema<A, I, R>
753
+ ): Schema.Schema<ReadonlySet<A>, ReadonlyArray<I>, R> =>
754
+ Schema.Array(itemSchema).pipe(
755
+ Schema.decodeTo(
756
+ Schema.ReadonlySet(Schema.toType(itemSchema)),
757
+ SchemaTransformation.transform({
758
+ decode: (items) => new Set(items),
759
+ encode: (set) => Array.from(set.values())
760
+ })
761
+ )
762
+ );
763
+
764
+ const schema = ReadonlySetFromArray(Schema.String);
765
+ // Schema<ReadonlySet<string>, readonly string[], never>
766
+ ```
767
+
768
+ ### Multi-Stage Transformations
769
+
770
+ ```typescript
771
+ import { Schema, SchemaTransformation } from 'effect';
772
+
773
+ const CentsFromDollars = Schema.Number.pipe(
774
+ Schema.decodeTo(
775
+ Schema.Number,
776
+ SchemaTransformation.transform({
777
+ decode: (dollars) => dollars * 100,
778
+ encode: (cents) => cents / 100
779
+ })
780
+ )
781
+ );
782
+ ```
783
+
784
+ ### Optional Field Transformations
785
+
786
+ v4 replaces `optionalToRequired`, `optionalToOptional`, and `requiredToOptional` with `Schema.decodeTo` + `SchemaGetter.transformOptional`:
787
+
788
+ ```typescript
789
+ import { Option, Predicate, Schema, SchemaGetter } from 'effect';
790
+
791
+ // optionalKey → required with default (null for missing)
792
+ const schema = Schema.Struct({
793
+ a: Schema.optionalKey(Schema.String).pipe(
794
+ Schema.decodeTo(Schema.NullOr(Schema.String), {
795
+ decode: SchemaGetter.transformOptional(
796
+ Option.orElseSome(() => null)
797
+ ),
798
+ encode: SchemaGetter.transformOptional(
799
+ Option.filter((value) => value !== null)
800
+ )
801
+ })
802
+ )
803
+ });
804
+ ```
805
+
806
+ ### Decoding Defaults
807
+
808
+ `Schema.withDecodingDefaultKey` / `Schema.withDecodingDefault` take defaults on the **Encoded** side. For `Schema.FiniteFromString`, that means a string default such as `'1'`.
809
+
810
+ `Schema.withDecodingDefaultTypeKey` / `Schema.withDecodingDefaultType` take defaults on the decoded **Type** side, such as `1`. Default effects may require services and may fail with `Schema.SchemaError`.
811
+
812
+ Keep fields required in the normalized decoded model when a default guarantees their value. Apply the default during construction or decoding; do not make domain values optional merely to make construction easier.
813
+
814
+ ```typescript
815
+ import { Effect, Schema } from 'effect';
816
+
817
+ const schema = Schema.Struct({
818
+ encodedDefault: Schema.FiniteFromString.pipe(
819
+ Schema.withDecodingDefault(Effect.succeed('1'))
820
+ ),
821
+ typeDefault: Schema.FiniteFromString.pipe(
822
+ Schema.withDecodingDefaultType(Effect.succeed(1))
823
+ ),
824
+ typeKeyDefault: Schema.FiniteFromString.pipe(
825
+ Schema.withDecodingDefaultTypeKey(Effect.succeed(10))
826
+ )
827
+ });
828
+
829
+ Schema.decodeUnknownSync(schema)({});
830
+ // { encodedDefault: 1, typeDefault: 1, typeKeyDefault: 10 }
831
+ Schema.decodeUnknownSync(schema)({ encodedDefault: '2' });
832
+ // { encodedDefault: 2, typeDefault: 1, typeKeyDefault: 10 }
833
+ ```
834
+
835
+ ## Common Patterns
836
+
837
+ ### Email Validation
838
+
839
+ ```typescript
840
+ import { Schema } from 'effect';
841
+
842
+ const Email = Schema.String.check(
843
+ Schema.isLowercased(),
844
+ Schema.isTrimmed(),
845
+ Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
846
+ );
847
+ ```
848
+
849
+ ### UUID Validation
850
+
851
+ ```typescript
852
+ import { Schema } from 'effect';
853
+
854
+ const UserId = Schema.String.check(Schema.isUUID()).pipe(
855
+ Schema.brand('UserId')
856
+ );
857
+ ```
858
+
859
+ ### Clamping Numbers
860
+
861
+ ```typescript
862
+ import { Schema } from 'effect';
863
+
864
+ const Percentage = Schema.Number.check(
865
+ Schema.isBetween({ minimum: 0, maximum: 100 })
866
+ ).pipe(Schema.brand('Percentage'));
867
+ ```
868
+
869
+ ### Template Literal Parsing
870
+
871
+ ```typescript
872
+ import { Schema } from 'effect';
873
+
874
+ // Parse Bearer tokens
875
+ const authTemplate = Schema.TemplateLiteral([
876
+ 'Bearer ',
877
+ Schema.String.pipe(Schema.brand('Token'))
878
+ ]);
879
+
880
+ const AuthToken = Schema.TemplateLiteralParser(authTemplate.parts);
881
+ // Decodes: "Bearer abc123" → ["Bearer ", "abc123"]
882
+ ```
883
+
884
+ ### Branded Types
885
+
886
+ ```typescript
887
+ import { Schema } from 'effect';
888
+
889
+ const PositiveInt = Schema.Number.check(
890
+ Schema.isInt(),
891
+ Schema.isGreaterThan(0)
892
+ ).pipe(Schema.brand('PositiveInt'));
893
+
894
+ // Type: number & Brand<"PositiveInt">
895
+ ```
896
+
897
+ ### Form Validation
898
+
899
+ ```typescript
900
+ import { Schema } from 'effect';
901
+
902
+ const LoginForm = Schema.Struct({
903
+ email: Schema.String.check(
904
+ Schema.isLowercased(),
905
+ Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
906
+ ),
907
+ password: Schema.String.check(
908
+ Schema.isMinLength(8),
909
+ Schema.isPattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
910
+ )
911
+ });
912
+ ```
913
+
914
+ ### API Response Parsing
915
+
916
+ ```typescript
917
+ import { Schema } from 'effect';
918
+
919
+ const User = Schema.Struct({
920
+ id: Schema.NumberFromString,
921
+ name: Schema.String,
922
+ email: Schema.String,
923
+ createdAt: Schema.DateTimeUtcFromString
924
+ });
925
+
926
+ const UsersResponse = Schema.Struct({
927
+ users: Schema.Array(User),
928
+ total: Schema.Number
929
+ });
930
+ ```
931
+
932
+ ## Quality Checklist
933
+
934
+ When creating schemas, ensure:
935
+
936
+ - [ ] Use `Schema.decodeTo` for type transformations, `.check()` for refinements
937
+ - [ ] Apply filters with `.check(Schema.isXxx())` — not the old `.pipe(Schema.xxx())` pattern
938
+ - [ ] Use `SchemaTransformation.transform` for custom transformations as first-class objects
939
+ - [ ] Extract reusable schemas as constants or factory functions
940
+ - [ ] Use `Schema.decodeUnknownEffect` directly in `Effect.flatMap` (no wrapper lambda)
941
+ - [ ] Place error mapping outside `flatMap` for cleaner composition
942
+ - [ ] Add annotations (`title`, `description`) to custom filters via `Schema.makeFilter`
943
+ - [ ] Use `Schema.toType` when composing to avoid double decoding
944
+ - [ ] Handle async operations with `Schema.decodeUnknownEffect`, not sync alternatives
945
+ - [ ] Return detailed error paths for form validation
946
+ - [ ] Use branded types for domain-specific values
947
+ - [ ] Use `schema.mapFields(Struct.pick(...))` instead of `schema.pick(...)`
948
+ - [ ] Use `schema.mapFields(Struct.omit(...))` instead of `schema.omit(...)`
949
+ - [ ] Use `schema.annotate({...})` instead of `schema.annotations({...})`
950
+ - [ ] Use `Schema.revealCodec(schema)` instead of `Schema.asSchema(schema)`
951
+
952
+ ## Key Principles
953
+
954
+ 1. **Composition over custom logic** — Leverage `Schema.decodeTo` and `.check()` instead of manual validation
955
+ 2. **Transformations are first-class** — Define with `SchemaTransformation.transform` and reuse across schemas
956
+ 3. **Reusability** — Extract schemas as constants or factory functions
957
+ 4. **Type safety** — Let Schema handle type inference and refinement
958
+ 5. **Streamlined Effect chains** — Minimize lambda wrappers, use direct function passing
959
+ 6. **Built-in filters first** — Use Effect's built-in `Schema.isXxx()` filters before creating custom ones
960
+ 7. **Parse, don't validate** — Transform data into the desired format, not just check it
961
+ 8. **Fail fast, fail clearly** — Provide detailed error messages with paths and context
962
+
963
+ ## References
964
+
965
+ - Effect Schema is imported from `effect/Schema` or `{ Schema } from "effect"`
966
+ - `SchemaTransformation` is imported from `effect/SchemaTransformation` or `{ SchemaTransformation } from "effect"`
967
+ - `SchemaGetter` is imported from `effect/SchemaGetter` or `{ SchemaGetter } from "effect"`
968
+ - `SchemaIssue` is imported from `effect/SchemaIssue` or `{ SchemaIssue } from "effect"`
969
+ - `Struct` is imported from `{ Struct } from "effect"` for `mapFields` operations
970
+ - Schema API signature: `Schema<Type, Encoded, Context>`
971
+ - All schemas return `readonly` types by default
972
+ - Use `Schema.revealCodec(schema)` to view any schema as `Schema<Type, Encoded, Context>`
973
+ - Use `Schema.toType(schema)` to get the type-side schema (replaces v3 `Schema.typeSchema`)
974
+ - Access struct fields with `.fields` property
975
+ - Filters preserve schema type — `.check()` on a `Schema.Struct` returns a `Schema.Struct`