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.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/guidance/effect-first-development.md +1247 -0
- package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
- package/guidance/post__parse-dont-validate.md +109 -0
- package/guidance/progressive-disclosure-guidance.md +38 -0
- package/package.json +63 -0
- package/patterns/avoid-any.md +37 -0
- package/patterns/avoid-data-tagged-error.md +34 -0
- package/patterns/avoid-direct-json.md +51 -0
- package/patterns/avoid-direct-tag-checks.md +54 -0
- package/patterns/avoid-expect-in-if.md +52 -0
- package/patterns/avoid-mutable-state.md +70 -0
- package/patterns/avoid-native-fetch.md +61 -0
- package/patterns/avoid-node-imports.md +86 -0
- package/patterns/avoid-non-null-assertion.md +44 -0
- package/patterns/avoid-object-type.md +46 -0
- package/patterns/avoid-option-getorthrow.md +39 -0
- package/patterns/avoid-platform-coupling.md +43 -0
- package/patterns/avoid-process-env.md +43 -0
- package/patterns/avoid-react-hooks.md +73 -0
- package/patterns/avoid-schema-suffix.md +45 -0
- package/patterns/avoid-sync-fs.md +68 -0
- package/patterns/avoid-try-catch.md +47 -0
- package/patterns/avoid-ts-ignore.md +38 -0
- package/patterns/avoid-untagged-errors.md +67 -0
- package/patterns/avoid-yield-ref.md +46 -0
- package/patterns/casting-awareness.md +46 -0
- package/patterns/context-tag-extends.md +84 -0
- package/patterns/effect-catchall-default.md +61 -0
- package/patterns/effect-promise-vs-trypromise.md +47 -0
- package/patterns/effect-run-in-body.md +58 -0
- package/patterns/imperative-loops.md +76 -0
- package/patterns/prefer-arr-sort.md +52 -0
- package/patterns/prefer-duration-values.md +56 -0
- package/patterns/prefer-effect-fn.md +161 -0
- package/patterns/prefer-match-over-switch.md +48 -0
- package/patterns/prefer-option-over-null.md +56 -0
- package/patterns/prefer-redacted-config.md +70 -0
- package/patterns/prefer-schema-class.md +54 -0
- package/patterns/require-effect-concurrency.md +83 -0
- package/patterns/stream-large-files.md +63 -0
- package/patterns/throw-in-effect-gen.md +62 -0
- package/patterns/use-clock-service.md +45 -0
- package/patterns/use-command-executor-service.md +54 -0
- package/patterns/use-console-service.md +54 -0
- package/patterns/use-filesystem-service.md +59 -0
- package/patterns/use-http-client-service.md +77 -0
- package/patterns/use-path-service.md +53 -0
- package/patterns/use-random-service.md +45 -0
- package/patterns/use-temp-file-scoped.md +66 -0
- package/patterns/vm-in-wrong-file.md +51 -0
- package/patterns/yield-in-for-loop.md +61 -0
- package/skills/effect-ai-chat/SKILL.md +472 -0
- package/skills/effect-ai-language-model/SKILL.md +652 -0
- package/skills/effect-ai-prompt/SKILL.md +752 -0
- package/skills/effect-ai-provider/SKILL.md +668 -0
- package/skills/effect-ai-streaming/SKILL.md +418 -0
- package/skills/effect-ai-tool/SKILL.md +1132 -0
- package/skills/effect-atom-rpc/SKILL.md +488 -0
- package/skills/effect-atom-state/SKILL.md +640 -0
- package/skills/effect-batching/SKILL.md +614 -0
- package/skills/effect-cache/SKILL.md +570 -0
- package/skills/effect-cli/SKILL.md +523 -0
- package/skills/effect-command-executor/SKILL.md +675 -0
- package/skills/effect-concurrency-testing/SKILL.md +612 -0
- package/skills/effect-config/SKILL.md +580 -0
- package/skills/effect-context-witness/SKILL.md +274 -0
- package/skills/effect-domain-modeling/SKILL.md +1212 -0
- package/skills/effect-domain-predicates/SKILL.md +867 -0
- package/skills/effect-error-handling/SKILL.md +1581 -0
- package/skills/effect-fiber/SKILL.md +731 -0
- package/skills/effect-filesystem/SKILL.md +624 -0
- package/skills/effect-graph/SKILL.md +571 -0
- package/skills/effect-http-api/SKILL.md +1760 -0
- package/skills/effect-http-client/SKILL.md +989 -0
- package/skills/effect-http-server/SKILL.md +920 -0
- package/skills/effect-incremental-migration/SKILL.md +362 -0
- package/skills/effect-layer-design/SKILL.md +642 -0
- package/skills/effect-managed-runtime/SKILL.md +395 -0
- package/skills/effect-mcp-server/SKILL.md +608 -0
- package/skills/effect-observability/SKILL.md +719 -0
- package/skills/effect-optics/SKILL.md +554 -0
- package/skills/effect-parallelization/SKILL.md +668 -0
- package/skills/effect-path/SKILL.md +296 -0
- package/skills/effect-pattern-matching/SKILL.md +914 -0
- package/skills/effect-platform-abstraction/SKILL.md +1175 -0
- package/skills/effect-platform-layers/SKILL.md +514 -0
- package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
- package/skills/effect-react-composition/SKILL.md +986 -0
- package/skills/effect-react-vm/SKILL.md +675 -0
- package/skills/effect-rpc-api/SKILL.md +624 -0
- package/skills/effect-rpc-client/SKILL.md +666 -0
- package/skills/effect-rpc-cluster/SKILL.md +1623 -0
- package/skills/effect-rpc-server/SKILL.md +767 -0
- package/skills/effect-scheduling/SKILL.md +124 -0
- package/skills/effect-schema-composition/SKILL.md +975 -0
- package/skills/effect-schema-v4/SKILL.md +691 -0
- package/skills/effect-scope/SKILL.md +682 -0
- package/skills/effect-service-implementation/SKILL.md +656 -0
- package/skills/effect-socket/SKILL.md +703 -0
- package/skills/effect-sql/SKILL.md +781 -0
- package/skills/effect-stream/SKILL.md +765 -0
- package/skills/effect-testing/SKILL.md +1331 -0
- package/skills/effect-typeclass-design/SKILL.md +161 -0
- package/skills/effect-wide-events/Article.md +66 -0
- package/skills/effect-wide-events/SKILL.md +95 -0
- package/skills/effect-workflow/SKILL.md +810 -0
- package/src/agent-policy.ts +22 -0
- package/src/enforcer.ts +104 -0
- package/src/frontmatter.ts +34 -0
- package/src/guidance.ts +66 -0
- package/src/index.ts +38 -0
- package/src/pattern-catalog.ts +115 -0
- package/src/pattern-matcher.ts +178 -0
- package/src/pattern.ts +97 -0
- package/src/skills.ts +29 -0
- 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`
|