A roblox-ts source file compiles against Luau’s global environment, not JavaScript’s: it calls
table.insert with no import, indexes strings from 1, and expects math.clamp. None of that is in
a browser.
Loom installs it. installGlobals(), injected ahead of your entry on both the dev server and a
static build, defines the whole standard library on globalThis before a single component
evaluates.
What is installed
| Global | Members |
|---|---|
table | insert, remove, find, concat, sort, create, clear, clone, freeze, isfrozen, pack, unpack, move, and the deprecated getn, maxn, foreach, foreachi |
string | lower, upper, len, reverse, char, byte, sub, rep, split, find, gsub, match, format |
math | abs, floor, ceil, sqrt, max, min, pow, exp, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, log, log10, ldexp, frexp, modf, sign, clamp, round, fmod, deg, rad, noise, random, randomseed, huge, pi |
os | clock, time, difftime, date |
bit32 | band, bor, bxor, bnot, btest, lshift, rshift, arshift, lrotate, rrotate, extract, replace, countlz, countrz, byteswap |
utf8 | char, codepoint, len, offset, charpattern, nfcnormalize, nfdnormalize |
debug | traceback, profilebegin, profileend, setmemorycategory, resetmemorycategory, info |
buffer | create, fromstring, tostring, len, the readi8…readf64 / writei8…writef64 pairs, readstring, writestring, copy, fill |
task | spawn, defer, delay, cancel, wait — mapped onto browser timers |
coroutine | create, resume, yield, wrap, status, running, isyieldable, close — real since 0.12.0 |
Random | A seeded generator with NextInteger / NextNumber / NextUnitVector / Clone. Added in 0.12.0 |
DateTime | now, fromUnixTimestamp, the format and component readers. Added in 0.12.0 |
| Free functions | select, unpack, rawget, rawset, rawequal, rawlen, typeOf, typeIs, pcall, xpcall, pairs, ipairs, next, tostring, tonumber, error, warn, print, assert, tick |
table counts from 1
table is not a roblox-ts macro. The compiler passes its arguments straight through to Luau, so
the number written in the source is already a Luau index. Loom reads it as one.
The array methods roblox-ts does compile as macros keep their 0-based TS indices. Both spellings appear in the same file all the time, and they drop the same element:
list.remove(0); // the macro — 0-based, like the TypeScript it is written intable.remove(list, 1); // the library — 1-based, like the Luau it compiles tostring.find has always behaved this way here for the same reason.
The tables themselves are ordinary JavaScript values: an array, a Map, a Set or a plain object.
So #list is list.length, and a hole does not end it.
sort takes a predicate
Luau’s comparator answers “does a come first”, not “which is bigger”:
table.sort(rows, (a, b) => a.order < b.order); // boolean, not -1 / 0 / 1A JS-style comparator returning a number is truthy for -1 and 1, so it sorts nothing in
particular. The default order, with no comparator, is < as in Luau. Since 0.12.0
Array.prototype.sort accepts the same boolean predicate, so a .sort() written in roblox-ts style
orders correctly too.
Where loom is forgiving
The engine raises an error in each of these. Loom leans the other way, so a preview renders rather than dying over an off-by-one:
| Case | Roblox | Loom |
|---|---|---|
insert at an out-of-range position | error | clamps into range |
remove at an out-of-range position | error | returns nil, mutating nothing |
concat over a non-string, non-number element | error | runs it through tostring |
freeze is Object.freeze: it stops writes to an array or a plain object, and cannot stop
Map.set or Set.add.
string patterns and tuple returns
find, match, gmatch and gsub take Luau patterns, translated to a regular expression —
character classes (%a, %d, %s, %w, …), anchors, quantifiers and captures. Indices are 1-based
and find accepts a negative init counting back from the end, as the engine does.
find, match and gmatch return their captures as the array roblox-ts reads a LuaTuple as, and
an empty one when nothing matched:
const [start, finish] = string.find(label, "%d+");const [name] = string.match(path, "([^/]+)$");for (const [word] of string.gmatch(sentence, "%a+")) { /* … */ }gsub takes a function or a table as its replacement, not only a string. format understands
the Lua specifiers a UI reaches for: width, padding and precision.
string.gsub("a b", "%a", (c) => c.upper()); // "A B", 2string.gsub("a b", "%a", { a: "X", b: "Y" }); // "X Y", 2string.format("%02d:%05.1f | %-10s|", 7, 3.14159, "ab");roblox-ts also calls these off a string receiver. String.prototype carries .lower(), .upper(),
.sub(), .rep(), .find(), .gsub(), .format(), .gmatch(), .byte(), .len(),
.reverse() and .size(). Each delegates to the library above, with the same 1-based indices and
tuple returns. .sub() deliberately replaces the Annex B HTML wrapper JavaScript ships under that
name.
String.prototype.match already exists in JavaScript with different semantics, on a prototype the
whole page shares — loom’s own code, React’s, your bundler’s runtime. Overwriting it to mean Luau’s
string.match would change behaviour far outside the previewed tree. Call string.match(s, …) instead. .split() is left native for the same family of reasons, since Luau’s string.split is implemented with it.
Array macros are patched the same way: .size(), .isEmpty(), .remove(i), .unorderedRemove(i)
and .clear() on Array.prototype, 0-based as roblox-ts compiles them. .size() and .isEmpty()
resolve on Map and Set through a symbol the preview rewrites previewed source to. Every other
Map on the page keeps plain JS semantics.
math.randomseed really seeds
Math.random cannot be seeded, so randomseed does not pretend to: it switches math.random over
to a deterministic generator. Code that seeds for reproducibility gets it, rather than being
silently ignored. A scene wanting the same “random” layout every reload works.
math.log takes an optional base, and frexp / modf return their pair as a destructurable array.
os.date
A strftime subset: %a %A %b %B %c %d %H %I %j %m %M %p %S %x %X %y %Y %%. An unrecognized
specifier is left as written. The *t and !*t table forms and the ! UTC prefix work on any
format. os.time accepts a date table as well as no argument, and os.difftime subtracts two of
its results.
debug profiling shows up in devtools
debug.profilebegin and profileend are wired to performance.mark and performance.measure.
Roblox instrumentation a component already carries appears as real entries in the browser’s
Performance panel. debug.traceback returns the JavaScript stack, which is the real one here.
bit32 and buffer details worth knowing
bit32 implements Luau’s shift semantics, not JavaScript’s. A shift of 32 or more saturates to
zero, where JS’s << masks the count to five bits and shifts by count % 32. Every result is an
unsigned 32-bit number.
buffer is little-endian and bounds-checked, so an out-of-range access throws as it does in the
engine. typeOf answers "buffer" for one, since Luau’s buffer is a distinct primitive type rather
than an ArrayBuffer with a different name.
The stubs
Installed so the code path exists, and doing nothing meaningful:
| Global | What it actually does |
|---|---|
math.noise | Returns 0 for every input. A UI that jitters with noise renders still rather than crashing. |
debug.info | The empty tuple. The engine reads its answers out of the Luau VM, which does not exist here, so callers destructure nils instead of crashing on a missing function. |
debug.setmemorycategory resetmemorycategory | Nothing. A browser has no Roblox memory categories to attribute to. |
task.wait | Returns a Promise, so await task.wait(n) works. A bare synchronous task.wait() cannot block in a browser and does not. |
What is deliberately absent
setmetatable, getmetatable, newproxy. Loom runs the author’s TypeScript, whose classes are
JavaScript classes. There is no faithful way to give a plain object a metatable’s __index
behaviour without proxying every table in the program. A half-faithful one would be worse than the
honest gap: a scene would render right up until the moment the metatable mattered.
print is installed but not declared. The runtime overwrites the value, so print works. The
ambient globals declaration omits it because lib.dom already declares one, and redeclaring is a
compile error. See TypeScript setup.
No filtering, no networking. Anything that would need a Roblox server throws by name rather than
quietly doing nothing. That covers HttpService.GetAsync and TextService.FilterStringAsync. See
Services.