Syntax Reference
Comments
Section titled “Comments”// Line comment/* Block comment *//* Nested /* block */ comments */Declarations
Section titled “Declarations”let x = 42let name: string = "hello"export let PI = 3.14159
// Destructuringlet (a, b) = pair // tuplelet { name, age } = user // record// (array destructuring not allowed in `let`; use `Array.get` or a match pattern)Function
Section titled “Function”let name(param: Type) -> ReturnType = { body}
// Generic function — type parameters after the namelet name<T>(param: T) -> T = { body}
let name<A, B>(a: A, b: B) -> (A, B) = { body}
export let name(param: Type) -> ReturnType = { body}
let name() -> Promise<T> = { expr |> Promise.await}
// async let sugar — `async let f() -> T = ...` means `let f() -> Promise<T> = ...`async let name() -> T = { expr |> await}// Recordtype User = { name: string, email: string,}
// Union — positional ( ) or named { } fieldstype Shape = | Circle(number) | Rectangle(number, number) | Named { width: number, height: number }
// Newtype (single-value wrapper)type OrderId = OrderId(number)
// String literal union (for npm interop)type HttpMethod = "GET" | "POST" | "PUT" | "DELETE"
// Structural aliastypealias Name = string
// Newtype (nominal)type UserId = UserId(string)
// Opaqueopaque type Email = Email(string)
// Deriving traitstype Point = { x: number, y: number,} deriving (Display)Use (Callback Flattening)
Section titled “Use (Callback Flattening)”// Single binding — rest of block becomes callback bodyuse x <- doSomething(arg)doStuff(x)
// Zero bindinguse <- delay(1000)Console.log("done")
// Chaininguse a <- first()use b <- second(a)result(b)For Block
Section titled “For Block”for Type { let method(self) -> ReturnType = { body }}
for Array<User> { let adults(self) -> Array<User> = { self |> Array.filter(.age >= 18) }}trait Display { let display(self) -> string}
// Trait with default implementationtrait Eq { let eq(self, other: Self) -> boolean let neq(self, other: Self) -> boolean = { !(self |> eq(other)) }}
// Implement a traitimpl Display for User { let display(self) -> string = { `${self.name} (${self.age})` }}Test Block
Section titled “Test Block”test "addition works" { assert add(1, 2) == 3 assert add(-1, 1) == 0}Expressions
Section titled “Expressions”Literals
Section titled “Literals”42 // number3.14 // number1_000_000 // number with separators (underscores for readability)3.141_592 // float with separators0xFF_FF // hex with separators"hello" // stringlet msg = `hello ${name}` // template literallet tagged = tag`a ${x} b` // tagged template literal — `tag` receives the strings and interpolated valueslet ok = true // booleanlet no = false // booleanlet arr = [1, 2, 3] // arrayUnderscores in number literals are purely visual — they are stripped during compilation. They can appear between any two digits but not at the start, end, or adjacent to a decimal point.
Tagged template literals compile to byte-identical TypeScript, so they interoperate cleanly with npm libraries that expose a tag`...` API (Drizzle’s sql, styled-components, emotion, graphql-tag). The tag must be a callable value; the runtime values at ${} are passed as the variadic ...values arguments.
Operators
Section titled “Operators”a + b a - b a * b a / b a % b // arithmetica == b a != b a < b a > b // comparisona <= b a >= b // comparisona && b a || b !a // logicala |> f // pipeexpr? // unwrapvalue |> transformvalue |> f(other_arg, _) // placeholdera |> b |> c // chainingvalue |> match { ... } // pipe into matchmatch expr { pattern -> body, pattern when guard -> body, _ -> default,}
// Pipe into matchexpr |> match { pattern -> body, _ -> default,}Patterns: literals (42, "hello", true), ranges (1..10), variants (Ok(x)), records ({ x, y }), string patterns ("/users/{id}"), bindings (x), wildcard (_), array patterns ([first, ..rest]).
Function Call
Section titled “Function Call”f(a, b)f(name: value) // named argumentf(a, b: 2, c: 3) // positional first, then namedConstructor(a: 1) // record constructorConstructor(a: 2, ..existing) // explicit fields first, spread lastCall rules:
- Positional arguments must precede named ones.
- Named arguments may appear in any order — the compiler reorders them to match the declaration.
- Every required parameter must be provided, either positionally or by name.
- A slot cannot be covered twice (positional + name for the same slot, or two named args with the same label).
- Defaulted parameters must be passed by name (not positionally) so a skipped default cannot silently shift a later value into the wrong slot.
Collect Block
Section titled “Collect Block”collect { let name = validateName(input.name)? let email = validateEmail(input.email)? ValidForm(name, email)}// Returns Result<T, Array<E>> — accumulates all errors from ?Constructors
Section titled “Constructors”Ok(value) // Result successErr(error) // Result failureSome(value) // Option presentNone // Option absentBuiltins
Section titled “Builtins”todo // placeholder, type never, emits warningunreachable // assert unreachable, type neverparse<T>(value) // runtime type validation, returns Result<T, Error>json |> parse<User>? // pipe form (most common)data |> parse<Array<Product>>? // validates arraysmock<T> // generate test data from type, returns Tmock<User>(name: "Alice") // with field overridesContextual keywords
Section titled “Contextual keywords”Some keywords are only reserved in specific positions so they don’t block
common names in user code (type as a JSON/DOM field, todo as a variable,
etc.). Outside those positions they parse as ordinary identifiers.
| Keyword | Acts as keyword when… |
|---|---|
type | Starts an item (type X = ..., including opaque type) |
opaque | Precedes type |
trusted | Modifies an import (import trusted { ... }) |
deriving | Tails a record type (... deriving (Display)) |
mock, parse | Followed by < (e.g. parse<User>(value)) |
collect | Followed by { (collect { ... }) |
todo, unreachable, clear, unchanged | No local binding with the same name is in scope |
// `type` as a record field and a parameter name — both finetype Message = { type: string, body: string,}
let make(type: string, body: string) -> Message = { Message(type: type, body: body)}
// JSX attribute<input type="text" />
// `todo` is shadowed by the local bindinglet todo = validateTodo(text)?saveTodo(todo) // reads the local — not the panic placeholderQualified Variants
Section titled “Qualified Variants”Filter.All // zero-arg variantFilter.Active // zero-arg variantColor.Blue(hex: "#00f") // variant with data
// Required when variant name is ambiguous (exists in multiple unions)// Ok, Err, Some, None are always bare (built-in)Anonymous Functions (Closures)
Section titled “Anonymous Functions (Closures)”(a: number, b: number) -> a + b(x: number) -> x * 2() -> doSomething()Dot shorthand for field access:
.name // (x) -> x.name.id != id // (x) -> x.id != id.done == false // (x) -> x.done == falseFunction Types
Section titled “Function Types”() -> () // takes nothing, returns nothing(string) -> number // takes string, returns number(number, number) -> boolean // takes two numbers, returns boolean<Component prop={value}>children</Component><div className="box">text</div><Input /><>fragment</>Imports
Section titled “Imports”import { name } from "module"import { name as alias } from "module"import { a, b, c } from "module"
// npm imports are untrusted by default — auto-wrapped in Result<T, Error>import { parseYaml } from "yaml-lib"let result = parseYaml(input) // Result<T, Error> — auto-wrapped
// trusted imports — safe to call directly, no wrappingimport trusted { useState } from "react"let (count, setCount) = useState(0)
// Per-function trustedimport { trusted capitalize, fetchData } from "some-lib"
// Import for-block functions by typeimport { for User } from "./helpers"import { for Array, for Map } from "./collections"
// Mix regular and for-importsimport { Todo, Filter, for Array } from "./todo"Exports
Section titled “Exports”// Named exports — prefix any top-level declarationexport let greet(name: string) -> string = { "hi ${name}" }export type User = { id: string, name: string }export let PI = 3.14
// Default export — only the bare-identifier form is allowed. Required by// Cloudflare Workers, Vite/Astro configs, Next.js pages, and anything else// that loads `default` by shape.let app = 42export default app// → compiles to: export { app as default };
// Anonymous default forms are parse errors — they are the TS foot-gun.// export default 1 // ❌// export default { k: v } // ❌ bind the object first// export default function() { } // ❌ (`function` is also banned)
// Re-export from another module without importing into scopeexport { Card, CardContent } from "@heroui/react"export { Todo as TodoItem } from "./types"At most one export default per module. The referenced name must be declared in the same module (let, type, or import). A non-exported local is fine — the export default itself counts as the export.
Patterns
Section titled “Patterns”Patterns appear inside match arms. The forms are:
42 // number literal"hello" // string literaltrue // boolean literalx // binding_ // wildcardOk(x) // variantSome(inner) // option{ field, other } // record destructure1..10 // range (inclusive)[] // empty array[only] // single-element array[first, ..rest] // array with rest"/users/{id}" // string pattern with captures_ when x > 10 // guard