Skip to content

Commit d939cbc

Browse files
authored
api(docs): simplifying after OpaqueRef annotations are no longer necessary on .map (commontoolsinc#1939)
docs: simplify OpaqueRef instructions after type inference improvements Update pattern/recipe documentation to reflect that OpaqueRef type annotations are no longer necessary in .map() callbacks. TypeScript now automatically infers the correct types. Changes: - Remove explicit OpaqueRef<T> annotations from all code examples - Update messaging from "must annotate" to "automatically inferred" - Note that explicit annotations still work if preferred - Celebrate the improvement as "good news" throughout Files updated: - docs/common/PATTERNS.md - docs/common/HANDLERS.md - docs/common/COMPONENTS.md - .claude/skills/recipe-dev/SKILL.md - .claude/skills/recipe-dev/references/workflow-guide.md 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent a91400c commit d939cbc

File tree

5 files changed

+32
-125
lines changed

5 files changed

+32
-125
lines changed

.claude/skills/recipe-dev/SKILL.md

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Begin with minimal viable recipe:
5151

5252
```typescript
5353
/// <cts-enable />
54-
import { Default, NAME, OpaqueRef, recipe, UI } from "commontools";
54+
import { Default, NAME, recipe, UI } from "commontools";
5555

5656
interface Item {
5757
title: string;
@@ -67,7 +67,7 @@ export default recipe<Input, Input>("My Recipe", ({ items }) => {
6767
[NAME]: "My Recipe",
6868
[UI]: (
6969
<div>
70-
{items.map((item: OpaqueRef<Item>) => (
70+
{items.map((item) => (
7171
<div>{item.title}</div>
7272
))}
7373
</div>
@@ -82,7 +82,7 @@ export default recipe<Input, Input>("My Recipe", ({ items }) => {
8282
Add bidirectional binding for simple updates:
8383

8484
```typescript
85-
{items.map((item: OpaqueRef<Item>) => (
85+
{items.map((item) => (
8686
<ct-checkbox $checked={item.done}>
8787
{item.title}
8888
</ct-checkbox>
@@ -137,7 +137,6 @@ See "Deployment Workflow" section below.
137137
### Common Error Categories
138138

139139
**Type Errors** (see `HANDLERS.md` for details):
140-
- Missing `OpaqueRef<T>` annotation in `.map()`
141140
- Wrong style syntax (object vs string, see `COMPONENTS.md`)
142141
- Using `Cell<OpaqueRef<T>[]>` instead of `Cell<T[]>` in handlers
143142
- Forgetting `Cell<>` wrapper in handler state types
@@ -167,7 +166,6 @@ See "Deployment Workflow" section below.
167166

168167
| Error Message | Check |
169168
|---------------|-------|
170-
| "Property X does not exist on type 'OpaqueRef<unknown>'" | Missing `OpaqueRef<T>` in `.map()` - See `HANDLERS.md` |
171169
| "Type 'string' is not assignable to type 'CSSProperties'" | Using string style on HTML element - See `COMPONENTS.md` |
172170
| Handler type mismatch | Check `Cell<T[]>` vs `Cell<Array<Cell<T>>>` - See `HANDLERS.md` |
173171
| Data not updating | Missing `$` prefix or wrong event name - See `COMPONENTS.md` |
@@ -256,20 +254,6 @@ const grouped = groupByCategory(items);
256254

257255
See `RECIPES.md` for reactive programming details.
258256

259-
### Type Annotations
260-
261-
**Always annotate `.map()` parameters:**
262-
263-
```typescript
264-
{items.map((item: OpaqueRef<Item>) => (
265-
<ct-checkbox $checked={item.done} />
266-
))}
267-
```
268-
269-
**Why:** TypeScript can't infer types for bidirectional binding without annotation.
270-
271-
See `PATTERNS.md` for common patterns.
272-
273257
## Multi-File Recipes
274258

275259
When building complex recipes across multiple files:
@@ -306,8 +290,6 @@ See `PATTERNS.md` Level 3-4 for linking and composition patterns.
306290
- Test syntax before deploying (unless deployment fails)
307291
- Add multiple features before testing
308292
- Use handlers for simple value updates
309-
- Forget `OpaqueRef<T>` annotations in `.map()`
310-
- Duplicate content from `docs/common/` - reference it instead
311293

312294
## Documentation Map
313295

.claude/skills/recipe-dev/references/workflow-guide.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ echo '{"title": "Test Item"}' | ./dist/ct charm set --identity claude.key --api-
111111
### Common Error Patterns
112112

113113
**Type Errors:**
114-
- Missing `OpaqueRef<T>` in `.map()` - See `HANDLERS.md`
115114
- Wrong style syntax (object vs string) - See `COMPONENTS.md`
116115
- Using `Cell<OpaqueRef<T>[]>` in handlers instead of `Cell<T[]>` - See `HANDLERS.md`
117116

docs/common/COMPONENTS.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ interface ShoppingItem {
5656
}
5757

5858
// In your recipe
59-
{items.map((item: OpaqueRef<ShoppingItem>) => (
59+
{items.map((item) => (
6060
<div>
6161
<ct-checkbox $checked={item.done}>
6262
<span>{item.title}</span>
@@ -66,8 +66,6 @@ interface ShoppingItem {
6666
))}
6767
```
6868

69-
**Important:** Notice the `OpaqueRef<ShoppingItem>` type annotation. This is required when using `.map()` with bidirectional binding to ensure proper type checking.
70-
7169
## When to Use Handlers vs Bidirectional Binding
7270

7371
### Decision Matrix
@@ -406,7 +404,7 @@ interface ShoppingItem {
406404
}
407405

408406
// ✅ CORRECT - Manual rendering for custom fields
409-
{items.map((item: OpaqueRef<ShoppingItem>) => (
407+
{items.map((item) => (
410408
<div>
411409
<ct-checkbox $checked={item.done}>
412410
{item.title}

docs/common/HANDLERS.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,8 @@ export default recipe<Input, Input>(
173173
return {
174174
[UI]: (
175175
<div>
176-
{/* Context 4: In JSX .map() - OpaqueRef<ShoppingItem> */}
177-
{items.map((item: OpaqueRef<ShoppingItem>) => (
176+
{/* Context 4: In .map() - item is OpaqueRef<ShoppingItem> */}
177+
{items.map((item) => (
178178
<ct-checkbox $checked={item.done}>{item.title}</ct-checkbox>
179179
))}
180180
<ct-button onClick={addItem({ items })}>Add</ct-button>
@@ -192,9 +192,9 @@ Think of types this way:
192192

193193
| Type | Mental Model | Where Used | Example |
194194
|------|--------------|------------|---------|
195-
| `Cell<T[]>` | A **box** containing an array | Handler params, recipe params, returns | `items: Cell<ShoppingItem[]>` |
195+
| `Cell<T[]>` | A **box** containing an array | Handler params, sometimes lift params, returns | `items: Cell<ShoppingItem[]>` |
196196
| `T[]` | The **plain array** inside the box | Result of `.get()` | `const arr = items.get()` returns `ShoppingItem[]` |
197-
| `OpaqueRef<T>` | A **cell-like reference** to each item | In JSX `.map()` | `items.map((item: OpaqueRef<ShoppingItem>) => ...)` |
197+
| `OpaqueRef<T>` | A **cell-like reference** to each item | Recipe params, in `.map()` | `items.map((item) => ...)` |
198198
| `T` | A **plain object** | Inside plain arrays | `{ title: "Milk", done: false }` |
199199

200200
### How They Transform
@@ -205,11 +205,9 @@ const items: Cell<ShoppingItem[]> // Handler receives this
205205
// Open the box with .get()
206206
const plainArray: ShoppingItem[] = items.get();
207207

208-
// In JSX, .map() wraps each item
209-
{items.map((item: OpaqueRef<ShoppingItem>) => (
210-
// item is NOT a plain ShoppingItem
211-
// item is NOT a Cell<ShoppingItem>
212-
// item IS an OpaqueRef<ShoppingItem>
208+
// In recipe, arguments, cells and map parameters are OpaqueRef<>
209+
{items.map((item) => (
210+
// item's type is automatically inferred as OpaqueRef<ShoppingItem>
213211
<ct-checkbox $checked={item.done} />
214212
))}
215213
```
@@ -219,7 +217,7 @@ const plainArray: ShoppingItem[] = items.get();
219217
**Different contexts require different types:**
220218

221219
1. **Handler parameters need `Cell<T[]>`** - so you can call `.get()` and `.set()`
222-
2. **JSX .map() needs `OpaqueRef<T>`** - for bidirectional binding to work
220+
2. **JSX .map() has `OpaqueRef<T>`** - for bidirectional binding to work (automatically inferred!)
223221
3. **Recipe schemas use plain `T[]`** - to define the data structure
224222

225223
**Common mistake:**
@@ -281,8 +279,8 @@ export default recipe<Input, Input>("Shopping List", ({ items }) => {
281279
return {
282280
[UI]: (
283281
<div>
284-
{/* In .map(): OpaqueRef<ShoppingItem> */}
285-
{items.map((item: OpaqueRef<ShoppingItem>) => (
282+
{/* Also in .map(), inferred as OpaqueRef<ShoppingItem> */}
283+
{items.map((item) => (
286284
<div>
287285
<ct-checkbox $checked={item.done}>
288286
{item.title}

0 commit comments

Comments
 (0)