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”Every word may name a property. JavaScript accepts any word as a property name and so does Floe, so a record field, a member, a named argument and a JSX attribute take any word. The three roles differ in one thing only: whether the word may also name a value.
| Role | Words | May name a value |
|---|---|---|
| Binding | type, opaque, trusted, collect, parse, mock, todo, unreachable, clear, unchanged | Yes |
| Property only | for, const, class, throw, null, undefined, any, as, enum, void, function, if, else, return | No: JavaScript reserves it |
| Keyword | let, fn, match, impl, when, self, import, export, trait, and the rest | No: Floe reserves it |
true and false are the one exception. Floe reads them as literals, not as
words, so { true: 1 } is legal JavaScript and is not legal Floe.
Binding words
Section titled “Binding words”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 placeholderAny word names a property
Section titled “Any word names a property”A record field, a member, a named argument, a dot shorthand and a JSX attribute all take any word.
type Payload = { for: string, class: string, function: string, if: string,}
let target(p: Payload) -> string = { p.for }
let view(p: Payload) -> JSX.Element = { <label for="name" class="row" /> }
let call() -> string = { render(for: "name", match: "exact") }
let targets(rows: Array<Payload>) -> Array<string> = { rows |> map(.for) }Words that cannot name a value
Section titled “Words that cannot name a value”A word JavaScript reserves cannot name a value, because the emitted
TypeScript would not compile: TypeScript reports TS1389 for a variable named
for and TS1390 for a parameter named for. A word Floe reserves cannot
either, because Floe needs it in its own position. Both report one error that
names the word:
let for = 1 // `for` is a reserved word in JavaScriptlet f(for: string) -> string = { "x" } // same, on the parameterlet match = 1 // `match` is a keyword in FloeRename the value instead.
A shorthand field is the one property position that also reads a value, so it
is narrower than the rest. { for } and Form { for } both read back as
for: for, so Floe reports the word and tells you to write the field out.
{ self } and { match ... } stay blocks rather than object literals, for
the same reason.
for also stays a keyword in three positions:
for Type { ... }at item startimport { for Type } from "..."impl Trait for Type { ... }
Qualified 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