Learn Lumen
A tour of the language in fourteen short stops. If you write TypeScript you already know the syntax; what changes is that every program is checked and compiled ahead of time to a native binary or a single WebAssembly module. Each sample was checked with the released compiler, and the ones marked Open in playground run in your browser.
Types you already know
Lumen is TypeScript syntax with the types checked ahead of time and
nothing dynamic left over. int (also spelled i32) is a
32-bit integer and number a 64-bit float; i64,
bool and string round out the scalars. Locals infer their
type from the initializer, and so do function return types, so annotations go
where they help a reader: parameters and public signatures.
Mixing int and number promotes to
number, and int widens losslessly to i64,
the way TypeScript's single number would let you expect.
// `int` is a 32-bit integer, `number` a 64-bit float. Locals and
// return types are inferred; annotate where it helps the reader.
const name: string = "Lumen";
let year = 2026; // int
let ratio = 0.75; // number
let ready: bool = true;
function describe(n: int) {
return `${name} ${n}`; // inferred: string
}
console.log(describe(year)); // Lumen 2026
console.log(ratio * 2); // 1.5
console.log(ready); // true
Records, unions and enums
Object shapes are closed: an interface or record
type says exactly which fields exist. A wider record still flows into
a narrower parameter (structural width subtyping). Unions are built from named
types, and a shared literal field such as kind makes them
discriminated, so switch narrows each branch and the compiler checks
that every variant is handled. Tuples are fixed-shape arrays like
[int, string]; enums are plain integer members.
interface Point {
x: int;
y: int;
}
type Circle = { kind: "circle"; radius: int };
type Square = { kind: "square"; side: int };
type Shape = Circle | Square; // discriminated union
enum Level { Low, High }
function area(s: Shape): int {
switch (s.kind) {
case "circle": return s.radius * s.radius * 3;
case "square": return s.side * s.side;
}
}
let p: Point = { x: 3, y: 4 };
let pair: [int, string] = [1, "one"];
console.log(p.x + p.y); // 7
console.log(area({ kind: "circle", radius: 2 })); // 12
console.log(pair[1]); // one
console.log(Level.High); // 1
Control flow
Everything you would write in TypeScript: if/else,
while, do, C-style for, for…of over
arrays, strings, Set and Map, for…in, the
ternary, labeled break/continue, and every compound
assignment (+=, ??=, <<=,
**=, …). A switch over an enum or a literal union is
exhaustive: a missing case is a compile error, and a function that returns from
every case needs no fallback return.
enum Color { Red, Green, Blue }
function name(c: Color): string {
switch (c) { // exhaustive: every enum member handled
case Color.Red: return "red";
case Color.Green: return "green";
case Color.Blue: return "blue";
}
}
let total = 0;
for (let i = 1; i <= 10; i += 1) {
if (i % 2 == 0) continue;
total += i;
}
console.log(total); // 25
for (const c of [Color.Red, Color.Blue]) {
console.log(name(c)); // red, blue
}
let n = 27;
let steps = 0;
while (n != 1) {
n = n % 2 == 0 ? n / 2 : 3 * n + 1;
steps += 1;
}
console.log(steps); // 111
outer: for (let a = 0; a < 3; a += 1) {
for (let b = 0; b < 3; b += 1) {
if (a * b == 2) { console.log(`${a}*${b}`); break outer; }
}
}
Null safety
There is no undefined. A value that may be absent is
T | null, an optional field port? is the same thing, and
??, ?. (including ?.[i], ?.() and
?.length) and the non-null assertion x! work on it. The
compiler narrows across if/else, guard clauses,
while, &&/||, and field paths like
a.b.c != null, so after the check you use the value as a plain
T.
interface Settings {
host: string;
port?: int; // optional field: int | null
}
function address(s: Settings): string {
let port = s.port ?? 8080;
return `${s.host}:${port}`;
}
function shout(word: string | null): string {
if (word == null) return "..."; // narrowed to string below
return word.toUpperCase();
}
let maybe: string | null = null;
console.log(maybe?.length ?? 0); // 0
console.log(address({ host: "localhost" })); // localhost:8080
console.log(address({ host: "api", port: 443 })); // api:443
console.log(shout("hey")); // HEY
console.log(shout(null)); // ...
Functions and closures
Default and rest parameters, spread at the call site, arrow
functions, function types, and closures that capture their surroundings.
Everything is checked statically, so there is no arguments object.
Array and object destructuring work in declarations, parameters and assignment,
with defaults ([a = 1], { x = 1 }).
One rule to know: a closure can read a captured variable but
not assign to it. The compiler reports E_CAPTURED_MUTATION and points
you at for…of or reduce instead, which keeps closures
cheap to compile to native code.
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
function sum(...nums: int[]): int {
let total = 0;
for (const n of nums) total += n;
return total;
}
function makeAdder(n: int): (x: int) => int {
return (x: int) => x + n; // a closure captures n
}
const applyTwice = (f: (n: int) => int, v: int): int => f(f(v));
let extra: int[] = [4, 5, 6];
let add10 = makeAdder(10);
let first = 10;
let second = 20;
[first, second] = [second, first]; // destructuring assignment
console.log(greet("World")); // Hello, World
console.log(sum(1, 2, 3, ...extra)); // 21
console.log(add10(5)); // 15
console.log(applyTwice((x: int) => x * 3, 2)); // 18
console.log(first); // 20
Classes and inheritance
class, extends, super,
public/private/protected, #private
fields, static, readonly, and get/set
accessors. A field with an initializer infers its type (count = 0 is an
int). Methods take default, optional and rest parameters like any
function, and method calls compile to direct calls on a flat struct: there is no
prototype chain to walk.
class Animal {
static created = 0;
protected name: string;
readonly legs: int;
constructor(name: string, legs: int) {
this.name = name;
this.legs = legs;
Animal.created += 1;
}
get label(): string { return `${this.name} (${this.legs} legs)`; }
speak(): string { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
constructor(name: string) { super(name, 4); }
speak(): string { return super.speak() + " (woof)"; }
}
let rex = new Dog("Rex");
console.log(rex.speak()); // Rex makes a sound (woof)
console.log(rex.label); // Rex (4 legs)
console.log(Animal.created); // 1
Generics
Generic functions, classes and interfaces, with constraints
(<T extends …>) and defaults (<T = …>). Each
instantiation is monomorphized at compile time, so Box<int> holds a
bare integer with no boxing. A method cannot yet introduce a type parameter of
its own; make the class generic or use a generic free function instead.
function firstOf<T>(xs: T[]): T {
return xs[0];
}
class Box<T> {
value: T;
constructor(v: T) { this.value = v; }
get(): T { return this.value; }
}
function largest<T extends int>(a: T, b: T): T {
return a > b ? a : b;
}
let boxed = new Box<int>(21);
console.log(firstOf<int>([10, 20, 30])); // 10
console.log(boxed.get() * 2); // 42
console.log(largest(3, 9)); // 9
Strings and regex
Single- and double-quoted literals, template literals with string
and numeric interpolation, and the methods you reach for: split,
slice, trim, includes, indexOf,
replace, repeat, padStart,
toUpperCase and more. Regex literals with .test() match
JavaScript's results exactly; anchored patterns compile to specialized native
matchers at build time, and everything else falls back to a built-in engine.
let csv = " ada, grace ,linus ";
let names = csv.trim().split(",");
for (const n of names) {
console.log(n.trim().toUpperCase().padStart(6, ".")); // ...ADA / .GRACE / .LINUS
}
console.log("hello".repeat(2).replace("lo", "p")); // helphello
console.log("grace,hopper".includes("grace")); // true
// Regex literals compile to native matchers at build time.
const semver = /^\d+\.\d+\.\d+$/;
console.log(semver.test("1.2.30")); // true
console.log(semver.test("1.2")); // false
Arrays, Map and Set
Arrays carry map, filter, reduce,
forEach, find, some, every,
includes, indexOf and join.
Map<K, V> and Set<T> are typed and iterable with
for…of. JSON.parse<T> takes the shape you expect and
returns exactly that: there is no untyped JSON value to inspect at runtime.
Arrays are fixed-length today. push, pop and
sort are on the roadmap; until then
build a new array with spread, map or filter.
let nums: int[] = [1, 2, 3, 4, 5, 6];
let evens = nums.filter((n: int) => n % 2 == 0);
let total = nums.reduce((acc: int, n: int) => acc + n, 0);
console.log(evens.map((n: int) => n * n).join(", ")); // 4, 16, 36
console.log(total); // 21
console.log(nums.some((n: int) => n > 5)); // true
let scores: Map<string, int> = new Map<string, int>();
scores.set("ada", 90);
scores.set("linus", 85);
for (const [who, score] of scores) {
console.log(`${who}: ${score}`);
}
let tags = new Set<string>();
tags.add("red");
tags.add("red");
console.log(tags.size); // 1
Errors, defer and tests
throw and try/catch/finally
propagate across functions, methods and constructors, with a readable
.message and a real stack trace on an uncaught error.
using _ = defer(…) runs cleanup when the enclosing scope exits, last
in first out. Tests live next to the code as test "…" { expect(…); }
blocks and run with lumen test file.ts; they are ignored by
lumen compile.
function parseAge(s: string): int {
let n = parseInt(s);
if (n == null || n < 0) throw new Error(`bad age: ${s}`);
return n;
}
function withFile(): void {
using _ = defer(() => console.log("closed")); // runs at scope exit, LIFO
console.log("open");
}
try {
console.log(parseAge("42"));
console.log(parseAge("-1"));
} catch (e) {
console.log(e.message); // bad age: -1
} finally {
withFile(); // open, closed
}
test "parseAge accepts digits" {
expect(parseAge("7") == 7);
}
Async, threads and events
async functions return Promise<T> and
await resolves them on a native event loop, alongside
setTimeout/setInterval. Worker.run(fn) starts a real
OS thread and hands back a promise for its result, so CPU-bound work runs in
parallel. EventEmitter<T> carries one payload type per instance.
These need the native target: the in-browser wasm build has no event loop yet.
async function fetchBase(): Promise<int> {
return 10;
}
async function total(n: int): Promise<int> {
let base = await fetchBase();
return base + n;
}
function fib(): i32 {
let a: i32 = 0;
let b: i32 = 1;
for (let i = 0; i < 40; i += 1) { const next = a + b; a = b; b = next; }
return a;
}
const emitter = new EventEmitter<string>();
emitter.on("greet", (msg: string) => console.log(msg));
emitter.emit("greet", "hello"); // hello
console.log(await total(32)); // 42
const p = Worker.run(fib); // a real OS thread
console.log("main keeps going");
console.log(await p); // 102334155
Calling C
Name a library with // @link, declare the
functions you use, and call them. Scalars and strings cross the boundary; the
compiler links the library into the binary. Packages such as
sqlite and quickjs
are built this way, and on the wasm target they embed the C library into the
module with a // @wasm-link archive. Details are in the
FFI reference.
// Name the library once; declare the C functions you use.
// @link m
declare function pow(base: number, exp: number): number;
declare function sqrt(x: number): number;
console.log(sqrt(pow(3.0, 2.0) + pow(4.0, 2.0))); // 5
Modules and URL imports
Default, named and import * as ns imports, plus
export … from re-exports, from a relative file or from an
https:// URL. A package is just a URL: the source is fetched at compile
time and compiled in, so there is no package manager, install step or lockfile.
The catalog lists what is available.
// A package is a URL: fetched over HTTPS and compiled in. No package
// manager, no lockfile. Relative imports work the same way.
import greet from "https://lumen-lang.org/package/std-contrib/hello/hello.ts";
import { render } from "https://lumen-lang.org/package/std-contrib/markdown/markdown.ts";
console.log(greet("world")); // Hello, world!
console.log(render("# Hi\n\n*md*"));
Build, run and test
One binary, six commands. lumen init writes a starter
project with a tsconfig.json so your editor's TypeScript tooling
works out of the box.
lumen init my-app && cd my-app
lumen check main.ts # type-check only
lumen run main.ts # build and run
lumen compile main.ts # native binary next to the source
lumen compile --release-fast main.ts
lumen compile --wasm main.ts # one self-contained .wasm (WASI)
lumen watch main.ts # rebuild and re-run on every save
lumen test main.ts # run the test blocks
Cross-compile with --target <triple>, ask for a
self-contained binary with --static, and link a vendored C library with
--link / --library-path. The install
section walks through each.
What is deliberately left out
Lumen removes the parts of JavaScript that need a runtime to
interpret them: no prototypes, no eval, no dynamic object shapes, no
arguments, no CommonJS, no any escape hatch. What remains
compiles to a small, dependency-free executable that starts in milliseconds.
The roadmap lists what is still being added.
Reference summary
The whole surface on one screen. The standard library reference covers the modules; this is the language itself.
- Static types
- Local inference and return-type inference for functions, arrows, and methods,
int/i64/number/bool/string, float & hex/binary literals. Numeric promotion (int+number) and losslessint→i64widening, just like TypeScript's singlenumber. - Records, interfaces & unions
- Closed object shapes, nested records, arrays of records,
enums, and fixed‑shape tuples like[int, string]. Structural width subtyping: a wider record flows into a narrower one, sointerfaceparameters accept any matching shape. Plustypealiases, discriminated and string‑literal unions,asassertions,as const,satisfies, andreadonlyarrays. - Control flow & operators
if/else if,while,do, C‑stylefor,for…of(arrays, strings,Set,Map),for…in,switch(exhaustive over literal unions and enums), ternary, and labeledbreak/continue. The full arithmetic, bitwise, and shift set, with every compound‑assignment form (+=,&&=,??=,<<=,**=, and more).- Null safety & narrowing
T | null, optional?fields,??, optional chaining?./?.[i]/?.()/?.length, and the non-null assertionx!. Flow narrowing acrossif/else, guard clauses (if (x == null) return),while,&&/||, and deep field paths (a.b.c != null) — plus discriminated-union complement narrowing.- Functions & parameters
- Function types, arrow functions, and capturing closures. Default values, rest parameters, and spread, checked statically, no
argumentsobject. Object shorthand{ x }and computed keys, destructuring defaults in array ([a = 1]) and object ({ x = 1 }) patterns, and destructuring assignment ([a, b] = [b, a]). - Classes & inheritance
extends,super,public/private/protected,#privatefields,static,readonly, andget/set. Fields infer their type from an initializer (count = 0); methods take default, optional, and rest parameters.- Generics
- Generic functions, classes, and interfaces with
Array<T>, monomorphized at compile time, no boxing. Type-parameter constraints (<T extends ...>) and defaults (<T = ...>). - Strings & regex
- Single‑ and double‑quoted literals, template literals
`hi ${name}`with string & numeric interpolation, and the methods you expect (split,slice,trim,includes,indexOf,replace,repeat,padStart,toUpperCase, and more)./pattern/literals with.test(): anchored patterns compile to specialized native matchers at build time, ~3× faster than V8 on checks like semver and identifiers, and everything else falls back to a built-in engine, so every pattern works. - Collections & helpers
Map<K, V>andSet<T>, both iterable withfor…of, and the array methods you expect (map,filter,reduce,forEach,find,some,every,includes,indexOf,join). PlusDate.now(),Object.keys/Object.freeze,Array.isArray/Array.from/Array.of,JSON.stringify/parse<T>,parseInt/parseFloat, and the fullMath/Numbersurface.- Errors, defer & tests
try/catch/finallyandthrow, propagating across functions, methods, and constructors; readable.message, optional catch binding, and full stack traces on uncaught errors. Scope‑exit cleanup withdefer(last‑in‑first‑out), and built‑in tests:test "…" { expect(…); }run withlumen test.- Async, threads & events
asyncfunctions returnPromise<T>andawaitresolves them on a fast native event loop, withsetTimeout/setIntervaland theirclear*counterparts.Worker.run(fn)spawns a real, detached OS thread — genuine CPU parallelism, with no per‑thread interpreter overhead to pay.EventEmitter<T>is statically typed, one payload type per instance, and ~3.5× faster than Node's on a tight emit loop.- Buffer, crypto & I/O
- A real byte‑array type, distinct from
stringat compile time:Buffer.from/.alloc,utf8/hex/base64encodings,.slice/.at/.equals.crypto.hmacSyncand AES‑256‑GCMencryptSync/decryptSyncboth read and return it. AlongsidenetTCP sockets, anhttpclient & concurrent server,process.stdin/stdout/stderras streams,readline.question, andzlibgzip/deflate. - Modules & native output
- Default, named, and
import * as nsimports, andexport … fromre-exports, from a relative file or anhttps://URL — a package is just a URL.declare function+// @linkcalls native C libraries, with scalars and strings marshalling across the boundary. It all compiles to a small, dependency‑free native binary.