Samples

Patterns that teach the language

Focused VexaScript snippets covering operator overloads, delegates, sync functions, ranges, extensions, and more.

Classes & Objects

Operator overloading

Operator and method overloading and new-less constructions, for concise writing.

class Vec2(val x: number, val y: number) {
  operator+(other: Vec2) => Vec2(x + other.x, y + other.y)
  operator-(other: Vec2) => Vec2(x - other.x, y - other.y)
}
Vec2(1, 2) + Vec2(3, 4)

JSX with Preact

Destructuring with types to avoid duplication while being concise.

import { h } from "preact"

fun Welcome({ name: string }) {
  return <section>Hello {name}</section>
}

Implicit property access

When no ambiguity happens, this is optional.

class Counter(var value: int) {
  fun increment(): int => ++value
}

Class delegates

Satisfy an interface by forwarding its members to another value using by.

interface Shape {
  area: number
  fill(color: string): string
}

class Rectangle(val width: number, val height: number) : Shape {
  area => width * height
  fill(color: string) => `${color}:${width}x${height}`
}

class ShapeLogger(val shape: Shape, val label: string) : Shape by { shape } {
  describe() => `${label}: area=${area}`
}

Delegated Properties

Tuple delegate

A [value, setter] tuple delegate wires reads and writes through custom accessors — like React's useState.

fun useState(value: number) {
  return [() => value, (newValue: number) => { value = newValue }]
}

var count by useState(0)
count = count + 1
count += 1
count++

Object & function delegates

A { value } object or a zero-argument function also work as delegates — all assignments route through the accessor.

fun box<T>(initial: T) {
  return { value: initial }
}

var source = 1
var observed by () => source   // function delegate: reads source
var total by box(0)            // object delegate: reads/writes .value

total = observed + 2
source = 5
total += observed
total++

Async & Sync

Sync functions

sync functions auto-await any Promise-typed expression. Write sequential code without explicit await.

sync fun fetchPrice(item: string): number {
  return fetch(`/prices/${item}`).json()
}

sync fun checkout(): number {
  const base = fetchPrice("book")   // auto-awaited
  const tax = fetchPrice("tax")     // auto-awaited
  return base + tax
}

The go operator

Inside a sync function, prefix an expression with go to keep the raw Promise instead of auto-awaiting it.

sync fun main(): void {
  // fire-and-forget — result Promise kept, not awaited
  const pending: Promise<number> = go fetchPrice("audit")

  // normal sync call — awaited automatically
  const price = fetchPrice("book")

  console.log(price)
  console.log(await pending)
}

Control Flow

Defer

defer schedules cleanup for the end of the block — it runs even when the block returns early or throws.

fun readValue(): int {
  console.log("open")
  defer console.log("close-2")
  defer console.log("close-1")
  console.log("read")
  return 7
}

Range expressions

... is end-inclusive; ..< is end-exclusive. Both work directly in for-of loops and as values.

for (n of 0 ..< 5) {
  console.log(n)    // 0, 1, 2, 3, 4
}

for (n of 1 ... 5) {
  console.log(n)    // 1, 2, 3, 4, 5
}

Smart casts

Inside if branches, is and in narrow the type of a stable identifier automatically.

class Cat { meow() {} }
class Dog { bark() {} }

fun greet(animal: Cat | Dog) {
  if (animal is Cat) {
    animal.meow()   // type narrowed to Cat here
  } else {
    animal.bark()   // type narrowed to Dog here
  }
}

fun clamp(value: int | string) {
  if (value in 0 ... 100) {
    const safe: int = value
  }
}

Tail lambdas

A lambda after the closing parenthesis — or as the only argument — follows Kotlin/Swift style and reduces visual noise.

val doubled = [1, 2, 3].map { it * 2 }

val even = [1, 2, 3, 4].filter { it % 2 == 0 }

val result = [1, 2, 3].map {
  const tripled = it * 3
  tripled + 1   // implicit return
}

Extensions & Calls

Extension properties

Add read-only properties to existing types. Import them where needed; access without an import is an error.

class Duration(val milliseconds: number)

val number.milliseconds => Duration(this)
val number.seconds: Duration => Duration(this * 1000)
val number.minutes: Duration => Duration(this * 60_000)

val d1 = 500.milliseconds
val d2 = 2.seconds
val d3 = 1.minutes

Generic extension methods

Extension methods can be generic and work with built-in collection types like Array<T>.

fun <T> Array<T>.second(): T => this[1]
val <T> Array<T>.doubledLength => length * 2

val xs = [10, 20, 30]
console.log(xs.second())        // 20
console.log(xs.doubledLength)   // 6

Named arguments

Pass arguments by parameter name in any order. The compiler reorders them to match the callee's parameter list.

fun connect(host: string, port: number, tls: boolean = false) {}

connect(port: 8080, host: "localhost")
connect("localhost", port: 8080, tls: true)

class Point(val x: number, val y: number)
val p = Point(y: 2, x: 1)

Function overloads

Multiple functions can share the same name when their parameter types differ. The compiler picks the right one at each call site.

function describe(value: int): string { return "int:" + value }
function describe(value: string): string { return "str:" + value }

console.log(describe(42))       // "int:42"
console.log(describe("hello"))  // "str:hello"