You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Prefer exact ReScript over a `string` placeholder — two fidelity improvements,
plus the optional-callback-param fix they share.
- Overloaded functions (intersection of call signatures, e.g.
`((reason?: bool|string) => void) & ((e: MouseEvent) => void)`) now become an
opaque module with one zero-cost `%identity` accessor view per signature
(`asReason`/`asMouse`) instead of silently dropping all but the first overload.
- Optional callback params (`(reason?: T) => void`) now map to `option<T> => unit`
rather than a required arg.
- `LiteralUnion | string` (e.g. `ToastPosition | string`, which TS collapses to
bare `string`) recovers the literals from the syntactic AST node and emits an
`@unboxed` variant with a `Custom(string)` catch-all (`<base>OrString`) —
typo-safe on the knowns, still open. Pure literal unions stay closed enums.
Contract: `%identity` is now sanctioned on opaque-module `as*` accessors too
(not just `from*`), as the fidelity fallback when `@unboxed` can't express a
shape. New golden cases `overload-intersection` and `literal-union-open`;
`docs/TYPE_MAPPING.md` + `CLAUDE.md` updated; golden contract linter allows `as*`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|`'a' \| 'b' \| (string & {})`| a plain enum — the `string & {}` "open" escape is dropped (this is the `HTMLInputTypeAttribute` shape) |
70
+
|`'a' \| 'b' \| string` (plain `\| string` widening) |`@unboxed type <base>OrString = \| @as("a") A \| @as("b") B \| Custom(string)` — the literals as `@as` arms + a `Custom(string)` catch-all. Zero-cost, typo-safe on the known values, still accepts any other string. (TS collapses the union to bare `string`, so the literals are recovered from the **syntactic** node — component-prop level only for now.) |
56
71
| real `enum`| same `@as` variant form |
57
72
58
73
Constructors are PascalCased from the literal (`"icon-only"` → `IconOnly`, `"2xl"` → `V2xl`). A
59
-
prop literally named `type` becomes `@as("type") ~type_` (reserved-word escaping).
74
+
prop literally named `type` becomes `@as("type") ~type_` (reserved-word escaping). The open
75
+
`<base>OrString` form is named to match the `boolOrString` / `stringOrNumber` convention, leaving the
76
+
bare name free for a co-occurring **pure** (closed) enum of the same literals.
77
+
78
+
> **`(string & {})` vs `\| string` — both keep the literals, differently.** The branded `(string & {})`
79
+
> form preserves its literals at the type level (TS does *not* collapse it) → a **closed** enum. The
80
+
> plain `\| string` form *is* collapsed by TS to `string`, so the literals are recovered from the
81
+
> syntactic union node → an **open**`@unboxed` variant with a `Custom(string)` catch-all. Caveat: a
82
+
> `\| string` may be a *genuine* escape hatch or merely *loose typing* (only the literals actually work
83
+
> at runtime) — a runtime question the type-only tool can't answer, so the catch-all (a strict superset
84
+
> of `string`) is the safe default; humans tighten to a closed enum when the runtime confirms it.
|`(reason?: boolean \| string) => void` (**optional** param) |`option<boolOrString> => unit` — an optional param becomes `option<…>` (`None` = omitted), never a required arg. |
137
+
|`((reason?: …) => void) & ((e: MouseEvent) => void)` (overload = intersection of call sigs, or a multi-call-signature interface) | an **opaque module with one zero-cost `%identity` accessor per signature** — `module CloseToastFunc = { type t; external asReason: t => (option<boolOrString> => unit) = "%identity"; external asMouse: t => (ReactEvent.Mouse.t => unit) = "%identity" }`; the prop is typed `…CloseToastFunc.t` with an `ⓘ` note. **No overload is dropped.** Falls back to 🔍 review only if a signature has an untypeable param. See [`overload-intersection`](../test/golden/cases/overload-intersection). |
111
138
112
139
Both forms work: an **inline** event param maps by the event's **name** (`MouseEvent`→`ReactEvent.Mouse.t`,
113
140
`ChangeEvent`→`ReactEvent.Form.t`, `KeyboardEvent`→`ReactEvent.Keyboard.t`, …); a `*EventHandler<T>`
@@ -202,7 +229,7 @@ becomes `JSON.t`, `keyof T` becomes `string`, and nested records carry the type
0 commit comments