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,554 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-optics
|
|
3
|
+
description: Use Effect Optic for composable, type-safe access and immutable updates to nested data structures. Covers Iso, Lens, Prism, Optional, and Traversal — when to use each, how to compose them, and practical patterns for deep updates, tagged unions, and filtered collections.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Effect TypeScript expert specializing in functional optics for immutable data access and transformation.
|
|
7
|
+
|
|
8
|
+
## Effect Source Reference
|
|
9
|
+
|
|
10
|
+
The Effect v4 source is available at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`.
|
|
11
|
+
Browse and read files there directly to look up APIs, types, and implementations.
|
|
12
|
+
|
|
13
|
+
Key reference files:
|
|
14
|
+
|
|
15
|
+
- `packages/effect/OPTIC.md` — full guide with examples
|
|
16
|
+
- `packages/effect/src/Optic.ts` — API surface and JSDoc
|
|
17
|
+
|
|
18
|
+
## Core Import
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { Optic } from 'effect';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
All optic types and constructors live under `Optic`. Supporting types come from `Result`, `Option`, and `Schema` as needed.
|
|
25
|
+
|
|
26
|
+
## Optic Type Hierarchy
|
|
27
|
+
|
|
28
|
+
Strongest to weakest — composing two optics produces the weaker kind:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Iso > Lens | Prism > Optional
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| Optic | get | set/replace | Use case |
|
|
35
|
+
| ------------- | ----------- | --------------------------- | --------------------------------------------------------- |
|
|
36
|
+
| **Iso** | Always | Always (no original needed) | Lossless two-way conversion (e.g. Celsius <-> Fahrenheit) |
|
|
37
|
+
| **Lens** | Always | Always (needs original `S`) | Always-present field in a struct |
|
|
38
|
+
| **Prism** | May fail | Always (no original needed) | Union variant, validated subset |
|
|
39
|
+
| **Optional** | May fail | May fail | General case — both reading and writing can fail |
|
|
40
|
+
| **Traversal** | Zero+ items | Zero+ items | Multiple elements in an array/collection |
|
|
41
|
+
|
|
42
|
+
**Traversal** is modeled as `Optional<S, ReadonlyArray<A>>` — not a separate optic kind. Use `.forEach()` and `.modifyAll()` to operate on individual elements.
|
|
43
|
+
|
|
44
|
+
## Starting an Optic Chain
|
|
45
|
+
|
|
46
|
+
Always begin with `Optic.id<S>()` — the identity Iso on type `S`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Optic } from 'effect';
|
|
50
|
+
|
|
51
|
+
type State = { user: { name: string; age: number } };
|
|
52
|
+
|
|
53
|
+
const _age = Optic.id<State>().key('user').key('age');
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Builder Methods (Chainable)
|
|
57
|
+
|
|
58
|
+
These are called on any optic instance to drill deeper or narrow focus:
|
|
59
|
+
|
|
60
|
+
### `.key(k)` — Lens into a struct/tuple field
|
|
61
|
+
|
|
62
|
+
Always-present field. Returns a Lens (from a Lens) or Optional (from an Optional). Does NOT work on union types.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type S = { readonly a: { readonly b: number } };
|
|
66
|
+
const _b = Optic.id<S>().key('a').key('b');
|
|
67
|
+
|
|
68
|
+
_b.get({ a: { b: 42 } }); // 42
|
|
69
|
+
_b.replace(99, { a: { b: 42 } }); // { a: { b: 99 } }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tuples use numeric keys:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
type S = readonly [string, number];
|
|
76
|
+
const _0 = Optic.id<S>().key(0);
|
|
77
|
+
_0.get(['hello', 42]); // "hello"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `.optionalKey(k)` — Lens that removes key on `undefined`
|
|
81
|
+
|
|
82
|
+
Like `.key()` but setting `undefined` removes the key from the struct (or splices from a tuple):
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
type S = { readonly a?: number };
|
|
86
|
+
const _a = Optic.id<S>().optionalKey('a');
|
|
87
|
+
|
|
88
|
+
_a.replace(2, {}); // { a: 2 }
|
|
89
|
+
_a.replace(undefined, { a: 1 }); // {}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `.at(k)` — Optional into a record/array index
|
|
93
|
+
|
|
94
|
+
For records or arrays where the key/index might not exist. Both get and set can fail. Always returns an Optional.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
type Env = { [key: string]: number };
|
|
98
|
+
const _x = Optic.id<Env>().at('x');
|
|
99
|
+
|
|
100
|
+
_x.replace(2, { x: 1 }); // { x: 2 }
|
|
101
|
+
// getResult fails if "x" is absent
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
type S = ReadonlyArray<number>;
|
|
106
|
+
const _0 = Optic.id<S>().at(0);
|
|
107
|
+
|
|
108
|
+
_0.replace(3, [1, 2]); // [3, 2]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### `.tag(variant)` — Prism into a tagged union variant
|
|
112
|
+
|
|
113
|
+
Narrows focus to the variant with the matching `_tag`. No-ops on non-matching variants:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
type Shape =
|
|
117
|
+
| { readonly _tag: 'Circle'; readonly radius: number }
|
|
118
|
+
| { readonly _tag: 'Rect'; readonly width: number };
|
|
119
|
+
|
|
120
|
+
const _radius = Optic.id<Shape>().tag('Circle').key('radius');
|
|
121
|
+
|
|
122
|
+
_radius.replace(10, { _tag: 'Circle', radius: 5 }); // { _tag: "Circle", radius: 10 }
|
|
123
|
+
_radius.replace(10, { _tag: 'Rect', width: 5 }); // { _tag: "Rect", width: 5 } (unchanged)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `.pick(keys)` / `.omit(keys)` — Lens into a subset of struct keys
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
type S = { readonly a: number; readonly b: number; readonly c: number };
|
|
130
|
+
|
|
131
|
+
const _ac = Optic.id<S>().pick(['a', 'c']);
|
|
132
|
+
_ac.replace({ a: 4, c: 5 }, { a: 1, b: 2, c: 3 }); // { a: 4, b: 2, c: 5 }
|
|
133
|
+
|
|
134
|
+
const _ac2 = Optic.id<S>().omit(['b']);
|
|
135
|
+
// same result
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `.notUndefined()` — Filter out `undefined`
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
const _defined = Optic.id<number | undefined>().notUndefined();
|
|
142
|
+
// getResult succeeds on 42, fails on undefined
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Calling `notUndefined()` on an `Optional` returns another `Optional`, not a `Prism`, because writing can still fail through the existing optional focus.
|
|
146
|
+
|
|
147
|
+
### `.check(...checks)` — Validate with Schema checks
|
|
148
|
+
|
|
149
|
+
Adds Schema validation. `getResult` fails when any check fails; `set` passes through unchanged:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { Optic, Schema } from 'effect';
|
|
153
|
+
|
|
154
|
+
const _pos = Optic.id<number>().check(Schema.isGreaterThan(0));
|
|
155
|
+
// getResult succeeds on 5, fails on -1
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `.refine(guard)` — Narrow by type guard
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
type B = { readonly _tag: 'b'; readonly b: number };
|
|
162
|
+
type S = { readonly _tag: 'a'; readonly a: string } | B;
|
|
163
|
+
|
|
164
|
+
const _b = Optic.id<S>().refine((s: S): s is B => s._tag === 'b', {
|
|
165
|
+
expected: `"b" tag`
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `.forEach(f)` — Traverse array elements
|
|
170
|
+
|
|
171
|
+
Available when focus is `ReadonlyArray<A>`. The callback receives an `Iso<A, A>` to drill into each element:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { Optic, Schema } from 'effect';
|
|
175
|
+
|
|
176
|
+
type S = { readonly a: ReadonlyArray<number> };
|
|
177
|
+
|
|
178
|
+
const _positive = Optic.id<S>()
|
|
179
|
+
.key('a')
|
|
180
|
+
.forEach((item) => item.check(Schema.isGreaterThan(0)));
|
|
181
|
+
|
|
182
|
+
_positive.modifyAll((n) => n + 1)({ a: [1, -2, 3] });
|
|
183
|
+
// { a: [2, -2, 4] }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `.compose(optic)` — Compose with another optic
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { Optic, Option } from 'effect';
|
|
190
|
+
|
|
191
|
+
type State = { value: Option.Option<number> };
|
|
192
|
+
|
|
193
|
+
const _inner = Optic.id<State>().key('value').compose(Optic.some());
|
|
194
|
+
// Optional<State, number>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Reading Values
|
|
198
|
+
|
|
199
|
+
| Method | Returns | When to use |
|
|
200
|
+
| --------------- | ------------------- | ------------------------------------ |
|
|
201
|
+
| `.get(s)` | `A` | Lens/Iso only — always succeeds |
|
|
202
|
+
| `.getResult(s)` | `Result<A, SchemaIssue.Issue>` | Any optic — explicit structured failure |
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const _a = Optic.id<{ a: number }>().key('a');
|
|
206
|
+
_a.get({ a: 1 }); // 1
|
|
207
|
+
_a.getResult({ a: 1 }); // Result.succeed(1)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
For traversals, use `Optic.getAll`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const getPositive = Optic.getAll(_positive);
|
|
214
|
+
getPositive({ a: [3, -1, 5] }); // [3, 5]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Writing Values
|
|
218
|
+
|
|
219
|
+
| Method | Behavior |
|
|
220
|
+
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
|
221
|
+
| `.replace(a, s)` | Returns new `S` with focused value replaced. Silently returns original on focus failure. |
|
|
222
|
+
| `.replaceResult(a, s)` | Returns `Result<S, SchemaIssue.Issue>` — explicit structured failure. |
|
|
223
|
+
| `.modify(f)` | Returns `(s: S) => S`. On focus failure, returns `s` unchanged. |
|
|
224
|
+
| `.modifyAll(f)` | Traversal only. Maps `f` over each focused element. |
|
|
225
|
+
| `.set(a)` | Prism/Iso only — builds `S` from `A` without needing original. |
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
// replace
|
|
229
|
+
_age.replace(31, state);
|
|
230
|
+
|
|
231
|
+
// modify (returns a function)
|
|
232
|
+
const inc = _age.modify((n) => n + 1);
|
|
233
|
+
inc(state);
|
|
234
|
+
|
|
235
|
+
// modifyAll (traversal)
|
|
236
|
+
const doubled = _positive.modifyAll((n) => n * 2);
|
|
237
|
+
doubled({ items: [1, -2, 3] }); // { items: [2, -2, 6] }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Standalone Dual Helpers
|
|
241
|
+
|
|
242
|
+
Every derived read/update operation also has a standalone dual function. Use these when composing data pipelines or when passing the operation as a value:
|
|
243
|
+
|
|
244
|
+
| Helper | Data-first | Data-last / pipeable |
|
|
245
|
+
| --------------------- | ----------------------------------------------- | -------------------------------------------- |
|
|
246
|
+
| `Optic.get` | `Optic.get(self, lens)` | `Optic.get(lens)(self)` |
|
|
247
|
+
| `Optic.getResult` | `Optic.getResult(self, optional)` | `Optic.getResult(optional)(self)` |
|
|
248
|
+
| `Optic.set` | `Optic.set(value, prism)` | `Optic.set(prism)(value)` |
|
|
249
|
+
| `Optic.replace` | `Optic.replace(self, optional, value)` | `Optic.replace(optional, value)(self)` |
|
|
250
|
+
| `Optic.replaceResult` | `Optic.replaceResult(self, optional, value)` | `Optic.replaceResult(optional, value)(self)` |
|
|
251
|
+
| `Optic.modify` | `Optic.modify(self, optional, f)` | `Optic.modify(optional, f)(self)` |
|
|
252
|
+
| `Optic.getAll` | `Optic.getAll(self, traversal)` | `Optic.getAll(traversal)(self)` |
|
|
253
|
+
| `Optic.modifyAll` | `Optic.modifyAll(self, traversal, f)` | `Optic.modifyAll(traversal, f)(self)` |
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { Optic, pipe } from 'effect';
|
|
257
|
+
|
|
258
|
+
type State = { readonly user: { readonly age: number } };
|
|
259
|
+
|
|
260
|
+
const age = Optic.id<State>().key('user').key('age');
|
|
261
|
+
const state: State = { user: { age: 30 } };
|
|
262
|
+
|
|
263
|
+
Optic.get(state, age); // 30
|
|
264
|
+
|
|
265
|
+
const older = pipe(
|
|
266
|
+
state,
|
|
267
|
+
Optic.modify(age, (value) => value + 1),
|
|
268
|
+
Optic.replace(age, 40)
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The standalone helpers delegate to the corresponding instance methods. `replace` / `modify` still return the original source on focus failure, while `replaceResult` / `getResult` retain the structured `SchemaIssue.Issue`.
|
|
273
|
+
|
|
274
|
+
## Constructors
|
|
275
|
+
|
|
276
|
+
For custom optics beyond the builder chain:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// Iso — lossless two-way conversion
|
|
280
|
+
const fahrenheit = Optic.makeIso<number, number>(
|
|
281
|
+
(c) => (c * 9) / 5 + 32, // get: Celsius -> Fahrenheit
|
|
282
|
+
(f) => ((f - 32) * 5) / 9 // set: Fahrenheit -> Celsius
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Lens — always-present focus, needs original for replace
|
|
286
|
+
const _first = Optic.makeLens<readonly [string, number], string>(
|
|
287
|
+
(pair) => pair[0],
|
|
288
|
+
(s, pair) => [s, pair[1]]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Prism — focus may not exist, set doesn't need original
|
|
292
|
+
import { Optic, Result, SchemaIssue } from 'effect';
|
|
293
|
+
|
|
294
|
+
const numeric = Optic.makePrism<string, number>((s) => {
|
|
295
|
+
const n = Number(s);
|
|
296
|
+
return Number.isNaN(n)
|
|
297
|
+
? Result.fail(new SchemaIssue.InvalidValue({ message: 'not a number' }))
|
|
298
|
+
: Result.succeed(n);
|
|
299
|
+
}, String);
|
|
300
|
+
|
|
301
|
+
// Prism from Schema checks
|
|
302
|
+
const posInt = Optic.fromChecks<number>(
|
|
303
|
+
Schema.isGreaterThan(0),
|
|
304
|
+
Schema.isInt()
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// Optional — both reading and writing can fail
|
|
308
|
+
const atKey = (key: string) =>
|
|
309
|
+
Optic.makeOptional<Record<string, number>, number>(
|
|
310
|
+
(s) =>
|
|
311
|
+
Object.hasOwn(s, key)
|
|
312
|
+
? Result.succeed(s[key])
|
|
313
|
+
: Result.fail(
|
|
314
|
+
new SchemaIssue.Pointer(
|
|
315
|
+
[key],
|
|
316
|
+
new SchemaIssue.MissingKey(undefined)
|
|
317
|
+
)
|
|
318
|
+
),
|
|
319
|
+
(a, s) =>
|
|
320
|
+
Object.hasOwn(s, key)
|
|
321
|
+
? Result.succeed({ ...s, [key]: a })
|
|
322
|
+
: Result.fail(
|
|
323
|
+
new SchemaIssue.Pointer(
|
|
324
|
+
[key],
|
|
325
|
+
new SchemaIssue.MissingKey(undefined)
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Since beta.105, all fallible optic operations use `SchemaIssue.Issue`, not `string`. Custom `makePrism` and `makeOptional` implementations must return structured issues. Issues do not format themselves through `toString`; use `SchemaIssue.makeFormatterDefault()` when a human-readable message is needed:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
const formatIssue = SchemaIssue.makeFormatterDefault();
|
|
335
|
+
const message = Result.match(_a.getResult({}), {
|
|
336
|
+
onSuccess: (value) => `value: ${value}`,
|
|
337
|
+
onFailure: formatIssue
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Built-in Prisms
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
// Option
|
|
345
|
+
Optic.some<A>(); // Prism<Option<A>, A> — focus on Some
|
|
346
|
+
Optic.none<A>(); // Prism<Option<A>, undefined> — focus on None
|
|
347
|
+
|
|
348
|
+
// Result
|
|
349
|
+
Optic.success<A, E>(); // Prism<Result<A, E>, A>
|
|
350
|
+
Optic.failure<A, E>(); // Prism<Result<A, E>, E>
|
|
351
|
+
|
|
352
|
+
// Record <-> entries
|
|
353
|
+
Optic.entries<A>(); // Iso<Record<string, A>, ReadonlyArray<readonly [string, A]>>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Schema Integration
|
|
357
|
+
|
|
358
|
+
Generate optics from Schema definitions with `Schema.toIso`:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { Schema } from 'effect';
|
|
362
|
+
|
|
363
|
+
const schema = Schema.Struct({
|
|
364
|
+
a: Schema.String,
|
|
365
|
+
b: Schema.Number
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const _b = Schema.toIso(schema).key('b');
|
|
369
|
+
_b.replace(2, { a: 'a', b: 1 }); // { a: "a", b: 2 }
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Works with class-based schemas too:
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
class Person extends Schema.Class<Person>('Person')({
|
|
376
|
+
name: Schema.String,
|
|
377
|
+
age: Schema.Number
|
|
378
|
+
}) {}
|
|
379
|
+
|
|
380
|
+
const _name = Schema.toIso(Person).key('name');
|
|
381
|
+
_name.replace('Bob', new Person({ name: 'Alice', age: 30 }));
|
|
382
|
+
// Person { name: "Bob", age: 30 }
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Practical Patterns
|
|
386
|
+
|
|
387
|
+
### Deep nested update (define once, reuse everywhere)
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { Optic, String } from 'effect';
|
|
391
|
+
|
|
392
|
+
interface Street {
|
|
393
|
+
readonly num: number;
|
|
394
|
+
readonly name: string;
|
|
395
|
+
}
|
|
396
|
+
interface Address {
|
|
397
|
+
readonly city: string;
|
|
398
|
+
readonly street: Street;
|
|
399
|
+
}
|
|
400
|
+
interface Company {
|
|
401
|
+
readonly name: string;
|
|
402
|
+
readonly address: Address;
|
|
403
|
+
}
|
|
404
|
+
interface Employee {
|
|
405
|
+
readonly name: string;
|
|
406
|
+
readonly company: Company;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const _streetName = Optic.id<Employee>()
|
|
410
|
+
.key('company')
|
|
411
|
+
.key('address')
|
|
412
|
+
.key('street')
|
|
413
|
+
.key('name');
|
|
414
|
+
|
|
415
|
+
// Reuse with different transforms
|
|
416
|
+
const capitalize = _streetName.modify(String.capitalize);
|
|
417
|
+
const upper = _streetName.modify((s) => s.toUpperCase());
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Tagged union — safe variant access
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
type Shape =
|
|
424
|
+
| { readonly _tag: 'Circle'; readonly radius: number }
|
|
425
|
+
| {
|
|
426
|
+
readonly _tag: 'Rect';
|
|
427
|
+
readonly width: number;
|
|
428
|
+
readonly height: number;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const _circleRadius = Optic.id<Shape>().tag('Circle').key('radius');
|
|
432
|
+
const _rectArea = Optic.id<Shape>().tag('Rect').pick(['width', 'height']);
|
|
433
|
+
|
|
434
|
+
// replace is a no-op on non-matching variants
|
|
435
|
+
_circleRadius.replace(10, { _tag: 'Rect', width: 5, height: 3 });
|
|
436
|
+
// { _tag: "Rect", width: 5, height: 3 } — unchanged
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Traversal with filtering
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import { Optic, Schema } from 'effect';
|
|
443
|
+
|
|
444
|
+
type S = {
|
|
445
|
+
readonly todos?: ReadonlyArray<{
|
|
446
|
+
readonly title?: string;
|
|
447
|
+
readonly description: string;
|
|
448
|
+
}>;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const _titles = Optic.id<S>()
|
|
452
|
+
.key('todos')
|
|
453
|
+
.notUndefined()
|
|
454
|
+
.forEach((item) => item.key('title').notUndefined());
|
|
455
|
+
|
|
456
|
+
const shout = _titles.modifyAll((t) => t.toUpperCase());
|
|
457
|
+
|
|
458
|
+
shout({
|
|
459
|
+
todos: [
|
|
460
|
+
{ title: 'milk', description: 'buy milk' },
|
|
461
|
+
{ description: 'buy bread' }
|
|
462
|
+
]
|
|
463
|
+
});
|
|
464
|
+
// { todos: [{ title: "MILK", description: "buy milk" }, { description: "buy bread" }] }
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Record traversal via entries
|
|
468
|
+
|
|
469
|
+
```ts
|
|
470
|
+
import { Optic, Schema } from 'effect';
|
|
471
|
+
|
|
472
|
+
const _positiveValues = Optic.entries<number>().forEach((entry) =>
|
|
473
|
+
entry.key(1).check(Schema.isGreaterThan(0))
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
const inc = _positiveValues.modifyAll((n) => n + 1);
|
|
477
|
+
inc({ a: 0, b: 3, c: -1 }); // { a: 0, b: 4, c: -1 }
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Debugging focus failures
|
|
481
|
+
|
|
482
|
+
Use `getResult` to see explicit success/failure:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
import { Optic, Result } from 'effect';
|
|
486
|
+
|
|
487
|
+
type S = { readonly a?: number };
|
|
488
|
+
const _a = Optic.id<S>().at('a');
|
|
489
|
+
|
|
490
|
+
const result = _a.getResult({});
|
|
491
|
+
Result.match(result, {
|
|
492
|
+
onSuccess: (value) => `value: ${value}`,
|
|
493
|
+
onFailure: () => 'no focus'
|
|
494
|
+
});
|
|
495
|
+
// "no focus"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Quick Reference Table
|
|
499
|
+
|
|
500
|
+
| Data shape | Builder method |
|
|
501
|
+
| -------------------------------------- | --------------------------------------------------- |
|
|
502
|
+
| Always-present field | `.key("field")` |
|
|
503
|
+
| Optional field (keep `undefined`) | `.key("field")` |
|
|
504
|
+
| Optional field (remove on `undefined`) | `.optionalKey("field")` |
|
|
505
|
+
| Union case by `_tag` | `.tag("Variant")` |
|
|
506
|
+
| Record/array index (may be absent) | `.at(key)` |
|
|
507
|
+
| Filter + update collection items | `.forEach(el => el.check(...))` / `.notUndefined()` |
|
|
508
|
+
| Subset of struct keys | `.pick([...])` / `.omit([...])` |
|
|
509
|
+
| Narrow by type guard | `.refine(guard)` |
|
|
510
|
+
| Option.Some | `.compose(Optic.some())` |
|
|
511
|
+
| Result.Success | `.compose(Optic.success())` |
|
|
512
|
+
|
|
513
|
+
## Known Limitations
|
|
514
|
+
|
|
515
|
+
- Only works with **plain JavaScript objects** and collections (structs, records, tuples, arrays). Class instances cause runtime errors on `replace`/`modify` (unless generated via `Schema.toIso` on a class schema).
|
|
516
|
+
- `.key()`, `.optionalKey()`, `.at()`, `.pick()`, `.omit()` do NOT work on union types (compile error). Use `.tag()` or `.refine()` first to narrow.
|
|
517
|
+
- No-op updates may still allocate a new root — do not rely on reference identity to detect no-ops.
|
|
518
|
+
- `replace` silently returns the original `S` when the optic cannot focus. Use `replaceResult` for explicit failure detection.
|
|
519
|
+
|
|
520
|
+
## Anti-Patterns
|
|
521
|
+
|
|
522
|
+
### WRONG: Repeating paths instead of defining an optic once
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
// Bad — duplicated navigation
|
|
526
|
+
const upper = {
|
|
527
|
+
...state,
|
|
528
|
+
user: {
|
|
529
|
+
...state.user,
|
|
530
|
+
profile: {
|
|
531
|
+
...state.user.profile,
|
|
532
|
+
name: state.user.profile.name.toUpperCase()
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
const lower = {
|
|
537
|
+
...state,
|
|
538
|
+
user: {
|
|
539
|
+
...state.user,
|
|
540
|
+
profile: {
|
|
541
|
+
...state.user.profile,
|
|
542
|
+
name: state.user.profile.name.toLowerCase()
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### RIGHT: Define the optic once, reuse for different transforms
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
const _name = Optic.id<S>().key('user').key('profile').key('name');
|
|
552
|
+
const upper = _name.modify((n) => n.toUpperCase())(state);
|
|
553
|
+
const lower = _name.modify((n) => n.toLowerCase())(state);
|
|
554
|
+
```
|