From rescript-lsp
ReScript 11/11.1 syntax features. Use when writing ReScript in projects using v11+. Check package.json dependencies for rescript version. Key features include customizable variants (@unboxed, @as, @tag), record type spread, async/await, and array spread syntax.
How this skill is triggered — by the user, by Claude, or both
Slash command
/rescript-lsp:rescript-11The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Variants can now map directly to JavaScript discriminated unions with zero runtime overhead.
Variants can now map directly to JavaScript discriminated unions with zero runtime overhead.
Strip wrapper tags, leaving only payloads:
@unboxed
type jsonValue =
| String(string)
| Boolean(bool)
| Number(float)
let values = [String("hello"), Boolean(true), Number(42.0)]
// Compiles to: ["hello", true, 42.0]
Create string enums that compile to their literal values:
@unboxed
type status = | @as("pending") Pending | @as("active") Active | @as("done") Done
let current = Active
// Compiles to: "active"
No toString/fromString needed—the variant IS the string at runtime.
Customize the discriminator property name:
@tag("type")
type event =
| @as("click") Click({x: int, y: int})
| @as("keydown") KeyDown({key: string})
let e = Click({x: 10, y: 20})
// Compiles to: {type: "click", x: 10, y: 20}
@unboxed
type nullable<'a> = Present('a) | @as(null) Null
let handleNullable = value =>
switch value {
| Present(v) => Some(v)
| Null => None
}
Extend record types by spreading fields:
type entity = {id: string, createdAt: Date.t}
type user = {...entity, name: string, email: string}
// user = {id: string, createdAt: Date.t, name: string, email: string}
Coerce between structurally compatible record types using :>:
type full = {id: string, name: string, extra: int}
type partial = {id: string, name: string}
let asFull: full = {id: "1", name: "test", extra: 42}
let asPartial = (asFull :> partial) // Zero-cost, type-level only
Use spread syntax instead of Array.concat:
let withNew = [...existing, newItem]
let prepended = [first, ...rest]
let combined = [...arr1, ...arr2]
External function calls now omit trailing undefined arguments automatically. This is important when binding to JavaScript APIs that behave differently based on argument count.
@val
external stringify: ('a, ~replacer: (string, JSON.t) => JSON.t=?, ~space: int=?) => string = "JSON.stringify"
let result = stringify(obj)
// Compiles to: JSON.stringify(obj)
// NOT: JSON.stringify(obj, undefined, undefined)
Common mistake: Forgetting that this behavior exists and manually handling undefined, or being surprised when a JS API works correctly without explicit undefined passing.
Inline record fields destructured via punning carry an anonymous type scoped to their constructor. The binding can't escape into another constructor position, even if the inline record shape is identical:
type state = Idle({items: array<item>}) | Active({items: array<item>})
// Wrong — `items` carries Idle's anonymous record type, can't be used in Idle(...)
switch (state, action) {
| (Idle({items}), Reset) => Idle({items}) // Type error!
}
// Correct — alias the field to avoid the escaping anonymous type
switch (state, action) {
| (Idle({items: i}), Reset) => Idle({items: i})
}
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub illusionalsagacity/claude-plugins --plugin rescript-lsp