FiveM TypeScript resources: fxmanifest, @citizenfx types and esbuild

Write FiveM resources in TypeScript or JavaScript: fxmanifest with dist files, @citizenfx types, an esbuild bundle, events, setTick and Wait, and calling Lua exports.

You want types, async/await and npm packages in your FiveM script, but your resource is just .lua files. FiveM runs JavaScript in its own runtime, so TypeScript works as long as it is compiled to JavaScript first. This guide gives the working layout: manifest, types, bundler, events and ticks, and how to mix it with Lua.

Project layout

text
my_resource/
  fxmanifest.lua
  package.json
  tsconfig.json
  src/
    client.ts
    server.ts
  dist/            (build output, loaded by FiveM)

You edit src/, build into dist/, and the manifest only knows about dist/.

The fxmanifest

lua
fx_version 'cerulean'
game 'gta5'

client_script 'dist/client.js'
server_script 'dist/server.js'

Both are plain JavaScript files. If your resource needs a particular JavaScript runtime version or Node features on the server, check the current Cfx.re docs for the manifest options, since the supported runtimes have changed over time.

Tip: if you leave out the build, FiveM says it cannot find dist/client.js. Always run the build before ensure or restart.

Install types and a bundler

bash
npm init -y
npm install --save-dev typescript esbuild @citizenfx/client @citizenfx/server

@citizenfx/client and @citizenfx/server are the type packages for the natives and Cfx.re functions on each side. A tsconfig.json that uses them:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

TypeScript only type checks here (noEmit). esbuild does the actual bundling:

json
{
  "scripts": {
    "build": "esbuild src/client.ts --bundle --outfile=dist/client.js --target=es2020 && esbuild src/server.ts --bundle --outfile=dist/server.js --target=es2020 --platform=node",
    "watch": "npm run build -- --watch",
    "typecheck": "tsc"
  }
}

Bundling means npm packages you import are copied into the single output file, so the server does not need node_modules at runtime. Check the Cfx.re docs for the right --platform and --target for the runtime your artifacts use.

Events

JavaScript has the same four building blocks as Lua, with other names:

Lua JavaScript
AddEventHandler on
RegisterNetEvent + handler onNet
TriggerEvent emit
TriggerServerEvent / TriggerClientEvent emitNet
ts
// src/server.ts
onNet('my_resource:requestData', () => {
  const src = (global as any).source as number
  emitNet('my_resource:data', src, { hello: 'world' })
})
ts
// src/client.ts
on('onClientResourceStart', (resourceName: string) => {
  if (GetCurrentResourceName() !== resourceName) return
  emitNet('my_resource:requestData')
})

onNet('my_resource:data', (data: { hello: string }) => {
  console.log(data.hello)
})

On the server, the player who triggered the event is in the global source, as in Lua. Read it into a constant at the start of the handler: after an await, the global may point at another player. The same security rules from securing server events apply, since the player can fire your onNet from outside. More on the event model in client and server events.

Ticks and waiting

There is no frame loop by default. setTick runs a function every frame, and Wait from Lua becomes a promise around setTimeout:

ts
const Delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))

setTick(async () => {
  const ped = PlayerPedId()
  const [x, y, z] = GetEntityCoords(ped, true)
  // ... check something about the position
  await Delay(500)   // without this, it runs every frame
})

Without a delay, the handler runs each frame, the same problem as a Lua loop without Wait(0): see Wait and threads explained. Use a longer delay when nothing needs to be checked every frame.

For a one-shot delay, skip setTick and just await Delay(1000) inside an async function.

Mixing with Lua exports

Exports work across languages. Register one in JavaScript:

ts
exports('getGreeting', (name: string) => `Hello ${name}`)

And call it from a Lua resource:

lua
local text = exports['my_resource']:getGreeting('Mic')

The other direction, calling a Lua export from TypeScript, goes through the exports object:

ts
const ok = exports['ox_inventory'].GetItemCount(source, 'bread')

Declare the shape yourself if the types do not know the other resource, for example with declare const exports: Record<string, any>. See exports in Lua for how they behave when the target resource restarts.

Build before you start

Run the watcher while you work, and restart the resource in the console after each build:

text
npm run watch
restart my_resource

If a restart does nothing new, check that the output file changed, that the manifest points at it, and that the resource is not served from a stale copy in cache/.

Checklist

Symptom Fix
FiveM cannot find dist/client.js Run the build; the manifest loads the output, not src/
Natives show as unknown in the editor Install @citizenfx/client and @citizenfx/server
import fails at runtime Bundle with esbuild so imports are inlined
Handler uses the wrong player Read source into a constant before any await
High resmon from a tick Add await Delay(ms) to the setTick handler
Lua cannot see your function Register it with exports('name', fn) and call it by resource name

Quick answers

Can I use TypeScript in a FiveM resource directly?

No. FiveM runs JavaScript. You write TypeScript, bundle it to plain JavaScript with a tool like esbuild, and point client_script and server_script at the output files.

Can a TypeScript resource call Lua exports?

Yes. Exports are shared across languages: call exports['my_resource'].functionName() from JavaScript, and Lua can call exports you register with exports('name', fn) in JavaScript.

What replaces Wait() in JavaScript?

Use a promise around setTimeout inside an async function or an setTick handler. The ticking loop itself is setTick, and each tick should await a delay so it does not run every frame.

Scripts that skip this problem

Quest CreatorA visual editor for quests and NPC dialogues, built node by node in game.View script β†’Item Creator V2Create usable items with animations, props, effects and more β€” without writing code.View script β†’Shop CreatorBuild a shop in under a minute β€” owners, employees, vaults and robberies included.View script β†’

Keep reading