TypeScript with Pulse
Pulse components are written in TypeScript and compiled to Lua by typescript-to-lua (TSTL). This page explains how that compilation model works, what TypeScript features are available, and what to watch out for.
Why TypeScript
- IntelliSense — VS Code autocompletes
signal,defineComponent,on.*, and all widget builders with full type hints - Type errors caught before build — wrong signal types, missing widget
.bind(), incorrect Roblox API usage all surface at edit time - Modern syntax — arrow functions, destructuring,
const/let, template literals, optional chaining — all compile correctly to Lua
How compilation works
rb build runs TSTL on every .ts file in your src/ folder and concatenates the Lua output into a single script. There is no TypeScript runtime — the output is pure Lua.
src/combat/SpeedHack.ts ──TSTL──▶ Lua chunk
src/pages/1_Combat.ts ──TSTL──▶ Lua chunk ──concat──▶ build/script.lua
src/misc/globals.lua ──────────────────────▶ Lua chunk
Each TypeScript file becomes an IIFE (immediately-invoked function) in the output — no module system at runtime, just flat compiled Lua.
Ambient globals — no imports needed
Every Pulse API (defineComponent, signal, on, toggle, slider, etc.) is declared as an ambient global in @rb-pulse/core. You get full IntelliSense without any import statements:
// No imports — these are global by declaration
defineComponent('SpeedHack', () => {
const enabled = signal(false) // ✓ autocompleted
const speed = signal(16)
on.heartbeat({ when: enabled }, () => {
const h = _PulseGetHumanoid() // ✓ typed as Humanoid | undefined
if (h) h.WalkSpeed = speed()
})
return [toggle('Speed Hack').bind(enabled)]
})
The @rb-pulse/core and @rbxts/types packages in your tsconfig.json types array provide all of this.
What works in TSTL
Most TypeScript features compile to idiomatic Lua:
// const and let
const enabled = signal(false)
let counter = 0
// Arrow functions
const double = (x: number) => x * 2
// Optional chaining
const h = char?.FindFirstChildWhichIsA('Humanoid')
// Nullish coalescing
const name = player.Name ?? 'Unknown'
// Template literals
Pulse.Log.info('ESP', `found ${count} targets`)
// Destructuring
const [x, y] = [pos.X, pos.Y]
// Interfaces (type-only, no runtime cost)
interface Target { model: Model; distance: number }
// Type assertions
const hrp = char.FindFirstChild('HumanoidRootPart') as BasePart
What does NOT work
TSTL compiles to Lua 5.1 — some TypeScript patterns have no Lua equivalent:
// ✗ No class inheritance on Roblox instances
class MyPart extends BasePart { } // TSTL doesn't support this
// ✗ No try/catch (use pcall instead)
try { riskyOp() } catch (e) { } // won't compile
// ✓ Use:
pcall(() => riskyOp())
// ✗ No async/await (Lua has no async model)
async function fetchData() { }
await fetchData()
// ✓ Use task.spawn + task.wait:
task.spawn(() => {
const data = fetchRemote()
})
// ✗ No runtime imports (everything is global)
import { something } from './other' // no module system at runtime
// ✗ No Map/Set methods that depend on JS runtime
const m = new Map<string, number>() // ✓ compiles fine
m.forEach(...) // ✓ works
m.size // ✓ works
Calling Roblox methods
Roblox methods use Lua colon syntax (instance:Method()). TSTL compiles TypeScript dot-call notation correctly for typed Roblox instances:
// TypeScript (dot notation) Compiled Lua (colon notation)
player.GetMouse() → player:GetMouse()
workspace.FindFirstChild('Enemies') → workspace:FindFirstChild("Enemies")
humanoid.GetState() → humanoid:GetState()
This works automatically as long as you're calling methods on properly typed Roblox instances from @rbxts/types. If you see a dot instead of colon in the Lua output for a Roblox method, it means the type isn't recognized — cast it:
const instance = something as BasePart
instance.Destroy() // now correctly compiled to :Destroy()
Null safety
TypeScript's undefined compiles to Lua nil. Use the character helpers instead of accessing LocalPlayer.Character directly:
// ✗ Direct access — can be nil at any time
const h = (game.GetService('Players').LocalPlayer.Character as Model)
.FindFirstChildWhichIsA('Humanoid') as Humanoid
// ✓ Pulse helpers — typed, nil-safe
const h = _PulseGetHumanoid() // Humanoid | undefined
if (!h) return
h.WalkSpeed = 50
| Helper | Type |
|---|---|
_PulseGetChar() | Model | undefined |
_PulseGetHRP() | BasePart | undefined |
_PulseGetHumanoid() | Humanoid | undefined |
_PulseGetAlive() | boolean |
Sharing code with globals.lua
Plain .lua files in src/ compile flat — their locals are upvalues accessible from all TypeScript components. Put shared game-specific helpers here:
-- src/misc/globals.lua
func.GetEnemies = function()
local folder = workspace:FindFirstChild("Enemies")
return folder and folder:GetChildren() or {}
end
func.IsEnemy = function(player)
-- your team logic
end
Call from TypeScript using the func global (declared as any in @rb-pulse/core):
const enemies = func.GetEnemies() as Model[]
const hostile = func.IsEnemy(player) as boolean
Type-checking without building
Run the TypeScript checker without triggering a full build:
rb check
This runs tsc --noEmit using your tsconfig.json and reports type errors. Run it before rb build to catch mistakes early — it's much faster than a full compile.
tsconfig.json
The tsconfig.json that rb init generates is pre-configured for Pulse:
{
"compilerOptions": {
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"target": "ESNext",
"moduleResolution": "bundler",
"types": ["@rb-pulse/core", "@rbxts/types"]
},
"include": ["src/**/*.ts"]
}
Key points:
"strict": true— catches nullable access, missing returns, and other common mistakes"noEmit": true— TypeScript only type-checks, TSTL does the actual compilation"types"— loads ambient globals from@rb-pulse/core(Pulse APIs) and@rbxts/types(Roblox APIs)
Don't change noEmit or types — both are required for the build to work correctly.
Common TypeScript errors in Pulse projects
Object is possibly undefined
const h = _PulseGetHumanoid()
h.WalkSpeed = 50 // ✗ error: h might be undefined
// ✓ fix:
if (h) h.WalkSpeed = 50
Property does not exist on type 'Instance'
const folder = workspace.FindFirstChild('Enemies')
folder.GetChildren() // ✗ error: FindFirstChild returns Instance | undefined
// ✓ fix:
const folder = workspace.FindFirstChild('Enemies') as Folder | undefined
if (folder) folder.GetChildren()
Type 'X' is not assignable to 'PulseSignal<boolean>'
const mode = signal('Normal')
toggle('Enable').bind(mode) // ✗ error: toggle needs boolean signal
// ✓ fix:
const enabled = signal(false)
toggle('Enable').bind(enabled)
Cannot find name 'signal'
Your tsconfig.json is missing "@rb-pulse/core" in types, or the package isn't installed. Run pnpm install in the project root.