This page covers the differences from TypeScript — enough for most humans.
See the full syntax reference for everything including TypeScript,
or the AI agent reference for a compact token-efficient version suited for AGENTS.md.
VexaScript vs TypeScript: Syntax Differences
This document summarises the syntax additions and differences that VexaScript introduces on top of TypeScript. Everything valid in TypeScript that is not overridden here continues to work the same way.
Character and string literals
As in TypeScript, single and double quotes both denote strings. VexaScript adds
a # prefix that turns either quoted form into an integer Unicode code point:
val text: string = 'aaa'
val letter: int = #'a' // 97
val emoji: int = #"😀" // 128512
val newline: int = #'\n' // 10
val matches = 'A'.charCodeAt(0) == #'A'
match (str.codePointAt(0)) {
#'\n' -> handleNewline()
else -> handleOther()
}
A #-prefixed literal must contain exactly one decoded Unicode code point.
#'aaa' and #"" are errors; the editor quick fix removes the prefix and
converts the value to a string. Character literals emit as direct integer
constants in JavaScript and C++, avoiding one-character string allocation. The
prefix is VexaScript-only; TypeScript quote behavior remains unchanged.
String-template shorthand
Backtick templates accept both the JavaScript-compatible ${expression} form
and a compact $identifier form. Escape a literal dollar sign as \$.
val name = "Ada"
val greeting = `Hello $name; next year you are ${age + 1}`
TypeScript requires ${name} for the first interpolation.
Variable declarations
Immutable declaration aliases
VexaScript accepts both const and val for immutable bindings. val is an
exact alias, while const is the default canonical spelling.
const name: string = "Ada" // default VexaScript and TypeScript spelling
val alias: string = "Ada" // equivalent VexaScript alias
Unlike TypeScript's broadly permissive local declaration rules, VexaScript
requires every val to be initialized in situ and checks that an uninitialized
local var is assigned on every path before it is read. A class var must also
be assigned by a field initializer, init block, or constructor on every path
before construction completes, even when the field is never read. The compound
keyword var! is the explicit escape hatch for initialization performed by
code the compiler cannot observe:
var value: int
if (ready) { value = 1 } else { value = 2 }
consume(value)
var! externallyInjected: Service
class Injected {
var! service: Service
}
Destructuring: :: for renaming and : for inline types
In VexaScript destructuring, the colon (:) introduces an inline type annotation, and the double-colon (::) renames a source property to a local binding. This is the reverse of TypeScript, where : renames.
// VexaScript
let { name :: displayName, age: number } = person
// ^^^ renames ^^^ type annotation
// TypeScript equivalent
let { name: displayName, age }: { name: string; age: number } = person
Functions
Function declaration aliases
VexaScript accepts func, fn, and fun as concise alternatives to
function. func is the default canonical spelling.
func add(a: number, b: number): number {
return a + b
}
fn subtract(a: number, b: number): number => a - b
fun legacyAlias(): void {}
Normalization quick fixes use const and func by default. The nearest
vexascript.json can override either choice:
{
"canonicalSyntax": {
"immutableDeclaration": "val",
"functionDeclaration": "fn"
}
}
=> shorthand for single-expression bodies
Named functions and class methods can use => to return a single expression, without a block body.
func double(x: number): number => x * 2
class Point(const x: number, const y: number) {
distanceTo(other: Point): number => Math.hypot(x - other.x, y - other.y)
}
sync functions (implicit auto-await)
The sync modifier declares a function that behaves like async but automatically awaits any Promise<T> sub-expression used as a value. The return type is written without the Promise<...> wrapper.
// VexaScript
sync func loadUser(id: string): User {
const data = fetchJson(`/users/${id}`) // auto-awaited; data: User
return data
}
// TypeScript equivalent
async function loadUser(id: string): Promise<User> {
const data = await fetchJson(`/users/${id}`);
return data;
}
Auto-await applies at every use site: call arguments, array elements, object properties, member receivers, operands. Bare local variable reads are not auto-awaited—only the point where a Promise is produced.
go operator (opt-out of auto-await)
Inside a sync function, prefix any expression with go to keep the Promise<T> value instead of awaiting it.
sync fun demo(): void {
val pending: Promise<User> = go fetchUser(id) // fire-and-forget or store
go fetchUser(id).then(process) // chain without awaiting
}
go is only valid inside sync functions and is a contextual keyword (existing go identifiers still work outside sync).
Named arguments
Arguments can be passed by parameter name in any order, freely mixed with leading positional arguments.
fun connect(host: string, port: number): void { }
connect(port: 8080, host: "localhost") // reordered automatically
connect("localhost", port: 8080) // mixed positional + named
TypeScript has no named-argument syntax.
Tail lambdas and brace lambdas
A lambda can be written after the closing parenthesis of a call (or omitting the parens entirely), following the Kotlin/Swift convention.
[1, 2, 3].map { it * 2 }
[1, 2, 3].filter { it > 1 }
[1, 2, 3].map { n: number -> n * 2 }
tasks.map() async { await run(it) }
tasks.map() sync { run(it) }
tasks.map(async { await run(it) })
tasks.map(sync { run(it) })
For a modified brace lambda, async or sync is written immediately before {, whether the lambda is trailing or remains inside the argument list. The modifier belongs to the lambda. async keeps TypeScript-style explicit await; sync enables VexaScript auto-await inside the callback. The trailing-lambda quick fix preserves the modifier when moving an argument-list lambda outside. TypeScript requires an arrow callback such as tasks.map(async it => await run(it)).
Brace lambdas are also valid anywhere an expression is accepted:
useEffect({
val timeout = setTimeout({
refresh()
}, 1000)
return { clearTimeout(timeout) }
}, [count])
When a brace lambda appears as a trailing lambda or as a brace-lambda argument, the shorthand { body } form gets the implicit it parameter. In ordinary expression positions, the same shorthand is a zero-argument lambda unless an explicit parameter list is written with ->.
Inside an argument list, { name } remains context-sensitive: it can be interpreted semantically as a one-parameter lambda (implicit it) when the expected parameter type is a function, and as a shorthand object literal otherwise. The explicit { arg -> body } form is always a lambda.
Object spread follows TypeScript compatibility rules for dynamic values:
{ ...value } accepts value: any (as well as object-shaped and unknown
values), while a statically known primitive spread is diagnosed.
TypeScript uses inline arrow functions: [1,2,3].map(it => it * 2).
Receiver function types and blocks
A function type can declare an implicit receiver before its parameters:
fun <T> T.configure(block: T.() -> void): T {
block(this)
return this
}
val point = Point(1, 2).configure {
x *= 2
y += x
}
T.(arg: A) -> R is represented at runtime as (T, A) -> R, so a direct call
passes the receiver first. Inside a contextually typed brace lambda, unqualified
members and this refer to the receiver. Implicit it is the first visible
parameter when one exists; for T.() -> R, it aliases the receiver. Nested
receivers can be selected explicitly with this@functionName.
Optional and non-null asserted member access use the same extension-method
resolution as ordinary member access. value?.method(args) guards the
extension call for a nullish receiver, while value!.method(args) resolves it
after removing nullish types.
@JsInline annotation
A bodyless function with @JsInline provides a raw JavaScript template inserted at each call site.
@JsInline("if (!cond) throw new Error(message)")
fun assert(cond: boolean, message: string = "assert failed")
assert(value > 0)
annotation declarations and @JsName
Annotations are declared explicitly and then applied with @:
annotation Benchmark
annotation JsName(const name: string)
annotation JsInline(const replacement: string)
Zero-argument annotations may omit parentheses in both declarations and use sites:
annotation Benchmark
@Benchmark
fun measure() {}
@JsName overrides the emitted JavaScript name while keeping the source name for VexaScript analysis.
@JsName("rgba")
class Color(val r: int, val g: int, val b: int, val a: int)
val white = Color(255, 255, 255, 255) // emits: new rgba(255, 255, 255, 255)
Annotations may also be attached directly to class fields, accessors, and methods. Their arguments are checked like top-level annotation applications and the annotations themselves are erased unless a compiler-recognized annotation defines lowering behavior.
Native implementation and FFI annotations
@CppHeader, @CppFlags, and @CppBody attach trusted C++ source and build
metadata to a signature-only function. @FFILibrary("candidate", ...) attaches
an ordered dynamic-library search list to an ambient class whose static methods
name C symbols. @FFIName optionally separates the imported symbol from the
source method name. Native builds use cached LibraryOpen symbol resolution;
JavaScript uses Deno FFI or a compatible globalThis.VexaFFI adapter. See
docs/syntax.md for @FFIStruct layouts, FFIPointer, asynchronous
Promise<T> calls, and the complete type and security contract.
Test files
vexa test discovers .test.vx files and supplies Node-compatible test and
strict assert helpers without imports:
test("arithmetic") {
assert(1 + 1 == 2)
}
The test name is reported by Node's test runner, and runner flags can be passed through the CLI.
Classes
Primary constructors
Classes can declare their constructor parameters inline after the class name (Kotlin-style). Parameters prefixed with val, var, let, or const become instance properties automatically.
class Point(val x: number, val y: number)
class User(val name: string, var age: int = 0)
Primary-constructor parameters can be forwarded directly to a base constructor.
The colon form and the canonical extends form are equivalent:
abstract class Entity(val id: int)
class User(id: int, val name: string) : Entity(id)
// formatted as: class User(const id: int, val name: string) extends Entity(id)
The arguments are type-checked against the base constructor and execute before
derived property initialization. An abstract class itself cannot be constructed
with either Entity(...) or new Entity(...); instantiate a concrete subclass.
// TypeScript equivalent
class Point {
constructor(readonly x: number, readonly y: number) {}
}
Optional braces for empty classes
When a class has no members (only the primary constructor or nothing), the braces can be omitted.
class Tag
class Point(val x: number, val y: number)
Class instantiation without new
A declared class can be called directly without new.
val p = Point(1, 2) // VexaScript
const p = new Point(1, 2); // TypeScript equivalent
new ClassName(...) is still valid and accepted.
VexaScript also checks each class-field initializer against its explicit field
type. Its distinct int type therefore rejects fractional number values:
class Invalid {
var count: int = 0.5 // error
}
Implicit this member access
Inside class methods and field initializers, class members can be referenced without the this. prefix. Local variables and parameters shadow members with the same name.
class Counter(val value: int) {
fun increment(amount: int): int {
return value + amount // emits: return this.value + amount
}
}
Instance initialization blocks
VexaScript classes support Kotlin-style init { } blocks, which TypeScript
does not have. Multiple blocks run once per instance in source order, after
field/primary-constructor initialization and before the explicit constructor
body. Class val fields still require an in-situ initializer; mutable var
fields can be initialized in these blocks.
class Counter {
val step = 1
var value: int
init {
value = step
}
}
JavaScript emission folds these statements into an instance constructor; the native backend emits the same initialization phase in the C++ constructor.
Explicit member kinds in classes and interfaces
Inside class and interface bodies, VexaScript also supports explicit member keywords so the declaration kind is visible at a glance. The older TypeScript-style member syntax still works; func and const/var are canonical by default, while fn, fun, function, val, and let remain supported.
interface Shape {
const area: number
func draw(ctx: CanvasCtx): void
}
class Rect {
const width: number
var height: number
func area(): number => width * height
}
Compound accessor blocks
A property may group its getter and setter under one declaration. The default
setter parameter is newValue; set(name) or set(name: Type) overrides it.
The order of get and set does not matter.
class Counter {
private var stored = 0
var value: int {
get { return stored }
set(next) { stored = next }
}
}
The form lowers through the same property-accessor path in JavaScript and C++.
Operator overloads
Classes can declare operator methods with the operator keyword.
class Vector(val x: number, val y: number) {
operator+(other: Vector): Vector => Vector(x + other.x, y + other.y)
operator*(scale: number): Vector => Vector(x * scale, y * scale)
}
val v = Vector(1, 2) + Vector(3, 4) // calls operator+
Computed index access can also be overloaded. operator[] receives the bracket dimensions in order. operator[]= receives the assigned value first, followed by the dimensions, which keeps multidimensional setters consistent.
class Array2<T>(val fallback: T) {
operator[](x: int, y: int): T => fallback
operator[]=(value: T, x: int, y: int): void { }
}
val array = Array2<string>("empty")
val cell = array[1, 2]
array[1, 2] = "next"
Variable-dimensional indexers use rest parameters:
class MultiArray<T>(val fallback: T) {
operator[](...dimensions: int[]): T => fallback
operator[]=(value: T, ...dimensions: int[]): void { }
}
val item = multi[1, 2, 3]
multi[1, 2, 3] = item
Extension index operators can also target Property<T> values produced by property references:
fun Property<number>.operator[](src: number, dst: number): TweenTarget => TweenTarget(this, src, dst)
tween(view::x[0, 100], time: 1.seconds)
TypeScript has no operator overloading, so equivalent code must use named methods such as get(x, y) and set(value, x, y).
VexaScript also supports the three-way comparison operator <=>. Primitive
numbers, big integers, strings, and characters produce a negative value, zero,
or a positive value. Classes and extensions may declare operator<=>; when a
direct <, <=, >, or >= overload is absent, those comparisons are derived
from its result. An operator== overload similarly derives != as its
negation. Ordering operators are rejected when neither a primitive ordering nor
an applicable direct or spaceship overload is defined.
Class interface delegates
A class can satisfy an interface by forwarding all missing members to a delegate value using by in the heritage clause.
interface Drawable {
draw(ctx: CanvasCtx): void
bounds: Rect
}
class Widget(val shape: Drawable) : Drawable by { shape } {
// draw() and bounds are forwarded to shape automatically
}
// TypeScript equivalent (written by hand)
class Widget implements Drawable {
constructor(private shape: Drawable) {}
draw(ctx: CanvasCtx) { this.shape.draw(ctx); }
get bounds() { return this.shape.bounds; }
}
Extension methods and properties
Methods and read-only properties can be added to existing types without modifying their class.
fun String.shout(): string { return this.toUpperCase() + "!" }
val number.seconds: Duration => Duration(this * 1000)
"hello".shout() // "HELLO!"
10.seconds // Duration(10000)
Extension members must be imported before use; they are not automatically in scope across files:
import { shout } from "./stringExtensions"
"hello".shout()
TypeScript has no first-class extension methods; the workaround is prototype augmentation, which is unsafe and not supported in strict mode.
Delegated variables
Variables can delegate reads and writes to an external object using by, inspired by Kotlin property delegates.
fun useState(initial: number) {
return [() => initial, (v: number) => { initial = v }]
}
var count by useState(0)
count++ // routes through the delegate setter
count += 5
The delegate shape determines the accessor logic:
| Delegate type | Read | Write |
|---|---|---|
[value, setter] |
first element | call second element |
[getter, setter] |
call first | call second |
Property<T> from expr::field |
.value getter |
.value = ... setter |
{ value: T } |
.value |
.value = ... |
() => T |
call function | — |
TypeScript has no delegated variable syntax.
Property references
expr::field captures a concrete property as Property<T>. At runtime the receiver is evaluated once and the property reference exposes name: string plus a get/set value: T property. It is intentionally different from the :: used inside destructuring patterns: in expression position it creates a bindable property reference, while in object binding patterns it renames a source property.
class View(var x: number)
val view = View(0)
val property = view::x
property.value = 1
var x by property
x = 100 // writes view.x
TypeScript has no direct property-reference expression. The closest equivalent is hand-written getter/setter closures.
Nullable types
VexaScript accepts T? as shorthand for T | undefined in every type position,
including nested generic arguments and explicit generic calls:
let users: User?[] = []
const context = createContext<User?>(undefined)
This suffix does not add null; write T | null or T | null | undefined
when those values are part of the type.
Numeric types
VexaScript extends the TypeScript type system with explicit integer types.
| Type | Description | TypeScript equivalent |
|---|---|---|
int |
32-bit integer | number |
number |
64-bit floating-point number | number |
numeric |
common supertype of int/number/long/bigint |
— |
long |
64-bit signed integer | bigint |
bigint |
arbitrary-precision integer | bigint |
long literals use the L suffix: 10L, 0xffL. At runtime, long values are lowered to JavaScript bigint with 64-bit wrapping (BigInt.asIntN(64, ...)).
int expressions are wrapped with |0 to keep the values int32.
val count: int = 0
val big: long = 9_223_372_036_854_775_807L
val ratio: number = 3.14
Statements
Range expressions
Ranges are first-class expressions with inclusive (...) and exclusive (..<) variants, inspired by Swift.
0 ... 10 // inclusive: 0 through 10
0 ..< 10 // exclusive: 0 through 9
Range iteration transpiles to a classic index loop:
for (n of 0 ..< 10) console.log(n)
// emits: for (let n = 0; n < 10; n++) console.log(n)
TypeScript has no range syntax.
defer statement
defer expression schedules a cleanup expression to run at the end of the current block, even if the block exits early via return or throw. Inspired by Swift and Go.
val file = open()
defer file.close()
return file.readAll()
// TypeScript equivalent
const file = open();
try {
return file.readAll();
} finally {
file.close();
}
for-in / for-of without declaration keyword
In VexaScript mode, loop iterators do not require a declaration keyword.
for (item of items) process(item)
for (key in map) use(key)
// TypeScript
for (const item of items) process(item);
for (const key in map) use(key);
Array comprehensions
VexaScript can collect a loop expression directly into a new array. TypeScript requires an explicit array operation or loop instead.
val doubled = [for (value of values) value * 2]
val normal = [for (n in 0 ..< 10) n]
val labels = [for (val [name, score] of entries) "$name:$score"]
val transformed = [for (n in 0 ... 9) if (n % 2 == 0) n else n * 3]
val optional = [for (n in 0 ... 9) if (n % 2 == 0) n]
val mixed = [1, for (n in 0 ... 9) n, ...items, for (n in 0 ... 9) n * 2, 0]
val conditionalMixed = [
for (n in 0 ... 9) if (n % 2 == 0) n else n * 3,
for (n in 0 ... 9) if (n % 2 == 0) n,
]
const doubled = Array.from(values, value => value * 2);
const normal = Array.from({ length: 10 }, (_, n) => n);
const labels = Array.from(entries, ([name, score]) => `${name}:${score}`);
const transformed = Array.from(range(0, 9), n => n % 2 === 0 ? n : n * 3);
const optional = Array.from(range(0, 9)).filter(n => n % 2 === 0);
const mixed = [1, ...range(0, 9), ...items, ...range(0, 9).map(n => n * 2), 0];
Inside an array comprehension, both in and of collect iterable values,
including values from inclusive and exclusive ranges. Comprehension in
follows declaration-free VexaScript loop semantics rather than JavaScript and
TypeScript object-key iteration. The single expression body determines the
result array's element type. Comprehensions can be interleaved with normal array
elements and spreads; each comprehension contributes all of its generated
values in place without requiring an explicit .... An if expression may be
the result expression. If a top-level result if omits else, it acts as a
filter: false iterations append nothing, and the true branch determines the
element type. Separating and trailing commas work normally when comprehensions
are array elements, even when the array begins with a conditional
comprehension.
VexaScript also allows an unbraced if without else as an element of an
ordinary array literal. It conditionally contributes one element, equivalent to
...(if (condition) [value] else []). The comma after the branch separates
array elements; an intentional comma expression must be parenthesized, such as
[(a, b)].
is nominal checks and built-in matcher patterns
The basic value is ClassName form remains VexaScript's shorter spelling of
instanceof. Both spellings perform the same nominal runtime check and narrow
stable identifiers and member expressions.
if (shape is Circle) {
shape.radius // shape is narrowed to Circle here
}
// `instanceof` has the same smart-cast behavior
if (shape instanceof Circle) {
shape.radius // shape is narrowed to Circle here too
}
// TypeScript equivalent
if (shape instanceof Circle) {
shape.radius;
}
Unlike instanceof, is additionally accepts built-in patterns:
if (result is ({ kind: "ok" } and { payload })) {
val kind: "ok" = result.kind
}
if (temperature is (>= 10 and < 20)) { log("mild") }
if (parts is ["start", ..., "end"]) { log("framed") }
if (path is /^\/users\/[0-9]+$/i) {
val text: string = path
}
Supported built-ins are primitive type tests, literal equality, object property
patterns, exact arrays, one standalone non-binding ... array wildcard,
relational patterns, and/or, and regular-expression literals. Primitive
patterns (string, number/int, boolean, and bigint/long) lower to
typeof rather than instanceof; class names remain nominal. Regex patterns
only match strings and accept no flags or the portable g/i flags. Computed
keys, object rest, array rest bindings, and custom matcher protocols are not
supported. Subjects and recursively inspected values are evaluated once in
both JavaScript and C++.
if and abrupt control flow as expressions
In VexaScript, if is an expression. A braced branch evaluates to its final
expression statement, an omitted else contributes undefined, and a branch
ending in return, throw, break, or continue has type never rather than
widening the reachable result.
val label = if (ready) "ready" else {
prepare()
"prepared"
}
val value = input ?? return fallback
The abrupt forms return, throw, break, and continue can appear wherever
an expression is accepted in .vx files. TypeScript keeps these forms
statement-only.
Match expressions
match is an expression lowered to the same typed if/else representation in
JavaScript and C++. Arms are ordered and braced bodies evaluate to their final
expression without a do keyword.
val label = match {
ready -> "ready" // `->` omits `when`
when retrying: "retrying" // `:` requires `when`
else -> "idle"
}
val result = match (value) {
{ kind: "ok", payload: val payload } -> payload
[string, val count: number, ...] -> "count=" + count
when /^error:/i: "error"
default -> "other"
}
Writing when condition -> body or condition: body is an error. else and
default are equivalent. A subject is evaluated once, and subject arms accept
the same built-in patterns as is, including >= 10 and < 20, objects, arrays,
the standalone ... wildcard, and regex literals. Successful arms preserve
their narrowed subject types; later arms retain only exclusions that are
logically definite.
Only subject match arms introduce bindings. val name captures with its
inferred narrowed type, while val name: Type checks that primitive or class
type and captures it. The name is scoped to that arm. Boolean is patterns
narrow existing values but do not declare bindings.
Postfix receiver blocks
The postfix form value. { ... } evaluates value once, makes it the implicit
receiver inside the block, and returns the same value. It is useful for grouped
configuration and mutation without introducing a temporary variable or an
apply helper.
val point = Point(10, 20). {
x *= 2
y += x / 2
}
VexaScript also supports value?. { ... }. This form evaluates value once,
executes the receiver block only when the value is not null or undefined,
and returns undefined for a nullish value. The block's receiver is narrowed to
the non-nullish type:
canvas.getContext("2d")?. {
fillStyle = "#f4f8fc"
}
Inside the block, x and y resolve against the Point receiver. The complete
expression still evaluates to that same Point instance. JavaScript and native
C++ emit the receiver block directly at the use site.
Cascade operator
VexaScript adds the cascade operator ... It evaluates a receiver once, applies each following member operation to that receiver, and returns the receiver.
val badge = Graphics()
..point = Vec2(centerX, centerY - 16)
..beginFill(0xff6b35)
..endFill()
// TypeScript equivalent
const badge = new Graphics();
badge.point = Vec2(centerX, centerY - 16);
badge.beginFill(0xff6b35);
badge.endFill();
TypeScript has no cascade operator.
Embedded XML / JSX
In VexaScript .vx files, JSX is always enabled. A < in expression position starts an XML/JSX element. Consequently, the angle-bracket type cast <Type>value is not available in VexaScript—use value as Type instead.
// VexaScript: JSX always on; use `as` for type casts
val name = maybeString as string
val elem = <div class="greeting">Hello {name}</div>
Unlike TSX attribute syntax, VexaScript permits an empty JSX expression
container. {} means undefined, so <button onAbort={} style={styles} />
parses and emits exactly as if onAbort={undefined} had been written; the
following style attribute remains a separate, normally parsed attribute.
Empty child containers likewise contribute undefined.
VexaScript also extends JSX child lists with Svelte-style control blocks. These
are not TypeScript/TSX syntax: {#for item of items}...{/for} produces a child
array, while {#if condition}...{:else if other}...{:else}...{/if} selects one
child sequence and yields null when no branch matches and no else is present.
<ul>
{#for item of items}
{#if item.visible}<li>{item.name}</li>{/if}
{/for}
</ul>
Component tags remain classic-factory calls. <MyComponent/> passes the
component reference to React.createElement (or the configured factory); a
renderer or an eager custom factory is responsible for invoking it.
// TypeScript: JSX opt-in (.tsx); angle-bracket cast available in .ts
const name = <string>maybeString; // .ts only
const elem = <div className="...">…</div>; // .tsx
/// documentation comments
VexaScript uses triple-slash (///) single-line doc comments in addition to /** */ block doc comments. Both are surfaced by the language server in hover and completion tooltips.
/// Returns the distance between two points.
/// [a] and [b] must be in the same coordinate space.
fun distance(a: Point, b: Point): number => Math.hypot(a.x - b.x, a.y - b.y)
Runtime namespaces
VexaScript namespace declarations create actual JavaScript objects at runtime. Exported members become object properties; non-exported members are private to the namespace closure.
namespace MathUtils {
const PI = 3.14159
export fun circleArea(r: number): number { return PI * r * r }
}
MathUtils.circleArea(5)
TypeScript namespaces are erased to IIFEs and are primarily a compile-time construct (their emitted objects are accessed through the same IIFE pattern, but VexaScript makes this the first-class runtime model).
Module imports
In addition to .vx, local runtime imports may target .ts, .tsx, .json,
and .txt. Appending ?text loads any local file as a string in both JavaScript
and native C++ builds:
import declarationSource from "./runtime.d.ts?text"
Text-module imports require exactly one default binding.
Module exports
In VexaScript .vx files, top-level runtime declarations are exported implicitly unless they are marked private, so export is optional for public top-level symbols:
fun greet(name: string): string => `Hello ${name}`
private fun hidden(): string => "secret"
import { greet } from "./helpers"
Explicit export is still supported and remains useful for default exports, re-exports, type-only exports, and teams that prefer the extra clarity at the declaration site.
Comment styles
VexaScript supports three comment styles (TypeScript supports only the first two):
// single-line comment
/// documentation comment (appears in hover/completion)
/* block comment */
Semicolons
Semicolons are optional in VexaScript. Newlines act as statement separators. This is by design—not just ASI—so idiomatic VexaScript code omits semicolons.
let a = 1
let b = 2
a += b