On this page

Data structures: what's built in

TypeScript ships a richer set of containers than Lua's single table, and the toolchain lowers each one to code Defold's Lua 5.1 VM runs. Most of them lower to a plain Lua table; the ones with behaviour (Map, Set, class, spread) lean on runtime helpers that TypeScriptToLua emits into lualib_bundle. That bundle is pure Lua 5.1 the CLI writes to the output root, pay-for-use: a container that needs a helper pulls it in, the rest cost nothing. Every container below runs unchanged on Defold's VM (LuaJIT on native and desktop, a 5.1 VM on HTML5).

This page is the full availability map. For the Lua-to-TypeScript container translation at cheat-sheet depth — which table shape becomes which TypeScript type — see TypeScript vs Lua.

The lowerings quoted below were captured by running the real transpile pipeline; the same shapes are pinned in packages/transpiler/src/data-structures-transpile.test.ts.

Built-in containers

Structure TypeScript form Lowers to Pulls lualib? Use instead / note
Array const xs: number[] = [1, 2, 3] Lua table {1, 2, 3} (1-based) Literal, index, push, length: no. Higher-order methods (map, filter, reduce, …): yes The idiomatic sequence. Keep arrays dense — holes desync arr.length from Lua's #.
tuple const t: [number, string] = [1, "a"] plain Lua table {1, "a"} No The cheapest container — a fixed-length, positional record. Prefer it over an object for a fixed pair.
Map new Map<string, number>() __TS__New(Map); .set/.get call helper methods Yes Arbitrary (incl. non-string, object) keys. Use over an object record when keys are not fixed strings. On hot paths, LuaMap (below) is the same idea with no lualib.
Set new Set<number>() __TS__New(Set) Yes Membership and dedup. Prefer over scanning an array for "is x present". On hot paths, LuaSet (below) skips lualib.
WeakMap new WeakMap<object, V>() __TS__New(WeakMap) Yes Per-object side data whose entries vanish when the key is collected — caches keyed by object identity.
WeakSet new WeakSet<object>() __TS__New(WeakSet) Yes Weak membership — "have I seen this object" without pinning it alive.
object record { x: 1, y: 2 } plain Lua table {x = 1, y = 2} No (spread / Object.entries pull a helper) Fixed, named string keys. The default for a struct; spread { ...a, y: 2 } lowers to __TS__ObjectAssign.
class class C { … } + new C() __TS__Class() + __TS__New; table + metatable, methods on prototype Yes Real OO with inheritance. On hot paths a plain object or a closure is lighter — reach for class when you want a typed constructor and method dispatch.

Two lowerings worth seeing in full. A tuple is just a table — no runtime helper, no require:

export const t: [number, string] = [1, "a"];
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
____exports.t = {1, "a"}
return ____exports

A class, by contrast, builds on the lualib runtime — __TS__Class makes the metatable-backed table and __TS__New instantiates it:

export class Counter {
  n = 0;
  bump(): void {
    this.n += 1;
  }
}
export const c = new Counter();
local ____lualib = require("lualib_bundle")
local __TS__Class = ____lualib.__TS__Class
local __TS__New = ____lualib.__TS__New
local ____exports = {}
____exports.Counter = __TS__Class()
local Counter = ____exports.Counter
Counter.name = "Counter"
function Counter.prototype.____constructor(self)
    self.n = 0
end
function Counter.prototype.bump(self)
    self.n = self.n + 1
end
____exports.c = __TS__New(____exports.Counter)
return ____exports

The require("lualib_bundle") only resolves at runtime because the CLI writes lualib_bundle.lua to the output root when any feature pulls it in. You never manage that file by hand.

Lower-overhead containers: the Lua table extensions

When a Map or Set sits on a hot path and the lualib __TS__New indirection matters, TypeScriptToLua's language extensions give you LuaTable, LuaMap, and LuaSet — thin, typed views over a raw Lua table that lower to direct indexing with no lualib:

const seen = new LuaSet<number>();
seen.add(1);
export const hit = seen.has(1);

const scores = new LuaMap<string, number>();
scores.set("a", 1);
export const a = scores.get("a");
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
local seen = {}
seen[1] = true
____exports.hit = seen[1] ~= nil
local scores = {}
scores.a = 1
____exports.a = scores.a
return ____exports

LuaSet.add becomes t[v] = true, LuaMap.set/get become plain t[k] = v / t[k], and nothing pulls lualib_bundle. The trade is a deliberately thin surface: get/set/has/delete (plus add on the set), and that is all — no .size, no for...of, no insertion order. Reach for the standard Map/Set when you need to enumerate or count; reach for LuaMap/LuaSet when you only get, set, and test membership and want zero runtime overhead.

Use them as ambient globals — do not import them. import { LuaMap } from "@typescript-to-lua/language-extensions" is rejected (… is not a module) and silently falls back to the heavyweight lualib Map; written bare, LuaMap/LuaSet/LuaTable are part of the global type surface like the engine namespaces. (The Lua standard library itself — typed by lua-types — adds no container types beyond the universal table; these extension types are the typed way to use that raw table directly.)

Not available — reach for instead

A couple of genuine JavaScript built-ins do not survive the transpile to Lua 5.1. They type-check but fail at compile time, so you find out from tsc --noEmit / the transpile diagnostics, not at runtime.

Want Status Reach for instead
Regular expressions — /b/, "x".match(/b/) Rejected — the diagnostic reads string.match is unsupported. String methods: indexOf, slice, startsWith, endsWith, includes, split. They cover most parsing needs and lower to native Lua string ops.
BigInt1n RejectedUnsupported node kind BigIntLiteral A number (Lua's double holds integers exactly up to 2^53), or a string when you only format/transport the value.

Regex is the one that bites most often. "x".match(/b/) type-checks but fails to transpile, because TypeScriptToLua maps String.prototype.match onto Lua's string.match, which takes a Lua pattern, not a JavaScript regex. Rewrite pattern checks with the string methods above; for anything more elaborate, parse by hand.

See also

  • TypeScript vs Lua — the container translation cheat sheet this page deepens.
  • Where script state lives — where to put these containers: per-instance self, a shared module local, or a module singleton.
  • TypeScript gotchas — the runtime sharp edges (truthiness, nil collapse) that bite once the data is in a container.