Skip to main content

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
HelperType
_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.