Back to blog
Jul 20, 2026
9 min read

Building a Dota 2 Custom Game in TypeScript with x-template

Why I ditched raw LUA for TypeScript when building IMBA Wars — a tour of XavierCHN's x-template,and the things that surprised me along the way

I’ve been building custom Dota 2 game on and off for years — first Nexus Defense in raw Lua, and now IMBA Wars, a personal project which i have been working on since Mid-May 2026 and will be release soon in its pre-alpha form. The biggest upgrade i have made was switching the entire codebase to TypeScript using XavierCHN’s x-template.

If you’re about to start writing a Dota 2 custom game in 2026, this article is my attempt to persuade you to use TypeScript instead of plain LUA.


What is x-template?

x-template is a full project template written in Chinese language (I am planning to make an english fork for this with a bit of my spice) for Dota 2 custom games built on two important pillars:

  1. TypeScriptToLua (TSTL) — Helps convert any Typescript into Lua, which is what the Dota 2 engine actually runs.
  2. react-panorama — your UI is written in React (yes, actual React components) and rendered inside Valve’s Panorama UI framework. RIP ark120202.

On top of that, the template wires up an entire development pipeline so you never have to think about it:

FeatureWhat it does
yarn devWatches everything — TS, LESS, Excel, KV — and recompiles on save
yarn launchBoots Dota 2 directly into tools mode with your addon loaded
Excel → KVGame data (abilities, heroes, items, units) lives in spreadsheets and auto-converts to Valve’s KeyValues format
KV → JSONThe same data is mirrored as JSON so both your game logic and UI can import it with types
CSV localizationOne CSV generates addon_english.txt, addon_schinese.txt, and friends
yarn prodProduction build with minification and optional Lua encryption for Workshop release

Clone it, run yarn install, and you have a working addon with auto hot-reload. No more manual symlink into dota_addons. And yes, it uses yarn v1.


Why TypeScript over LUA?

The Dota 2 API is enormous, and Lua won’t help you with it

Dota 2 scripting API contains thousands of functions, and LUA won’t be able to help you with any error. Is it GetAbilityByIndex or GetAbilityAtIndex? Does FindUnitsInRadius take 9 arguments or 10, and in what order? In LUA , you will only find out any errors while playing the game which triggers that particular code. The loop to test for any bugs/errors is gonna cost so much of your time.

With TSTL, the community-maintained dota-lua-types and panorama-types packages give you full autocomplete and type-checking for the entire engine API. Typos and wrong argument orders will be much more obvious for you.

nil errors at compile time, not in your playtest

Most common issue is when calling a method on nil such as a unit that died, an ability that wasn’t found, a player that disconnected. LUA will happily allow you write that code, compile, and only explodes when the game hits a bad code.

TypeScript’s strict null checking forces me to handle these cases when I write the code:

const hero = PlayerResource.GetSelectedHeroEntity(playerId);
if (!hero) return; // TS forces me to deal with the undefined case

hero.AddNewModifier(hero, undefined, 'modifier_imba_unleashed', {});

This single feature has saved me more debugging hours than everything else combined.

Classes and decorators instead of meta-table rituals

It’s very easy to register an ability via class:

@registerAbility()
export class imba_tower_hex extends BaseAbility {
    GetIntrinsicModifierName(): string {
        return modifier_imba_tower_hex.name;
    }
}

For example, the above custom modifier (A tower ability which hexes any nearby enemy) that belongs to this ability lives in the same file, as another class. No more digging through class.lua implementations like an archeologist.

One set of types shared between client and server

Now, this is the best FEATURE. In Dota 2, the server and the UI (Panorama) communicate through custom game events and net tables, and in LUA + Javascript — rename a field on the server and the UI silently reads undefined.

In x-template, both sides import shared .d.ts declaration files. When the server pushes hero selection state (my custom built hero selection phase) into a net table, my React component receives it with the exact same type:

// shared/net_tables.d.ts — one definition, two consumers
interface HeroSelectionState {
    choices: string[];
    redrawsLeft: number;
    lockedHero?: string;
}

Change the shape on one side and the other side fails to compile. In my IMBA Wars project where the HUD, hero selection, and end screens are all driven by server state, this is the biggest difference between refactoring confidently and not blindly.

React for Panorama is a genuine superpower

Valve’s native Panorama workflow is XML layouts plus explicit JavaScript that manually finds and mutates panels. It works, but that’s the old school jQuery-era development.

With react-panorama, my entire HUD are components and hooks. The template even ships a useXNetTable hook, so subscribing to live server state looks like any modern React app:

const heroState = useXNetTable('hero_selection', 'state', defaultState);
return <Panel className="pickGrid">
    {heroState.choices.map(hero => <HeroCard key={hero} name={hero} />)}
</Panel>;

When the server updates the net table, the component re-renders. No manual panel bookkeeping, no stale UI.


What You Get Beyond the Language

A few more features in the template that I now refuse to live without:

  • Excel as a game-design database. All ability values, hero stats, and item definitions live in .xlsx files that compile to KV. Balancing a number means editing a cell, not hunting through nested KV braces. Localization tags embedded in the sheets export automatically.
  • An in-game Jest-style test framework. IMBA Wars runs describe/it/expect test suites inside the running game via a chat command. Async tests with timers work. Testing game logic without this is “load the map and click around for 10 minutes.”
  • Flame graph profiler. When the server hitches, I can see exactly which LUA functions are eating frame time, rendered as a flame graph in the UI.
  • Encrypted publishing. yarn prod compiles, minifies, and AES-encrypts the LUA output so your Workshop release isn’t trivially decompiled. Getting the dedicated server key is a one-time dance, and then it’s automatic.

The Honest Downsides

It’s not free lunch, and pretending otherwise would be dishonest:

You’re debugging compiled Lua. When something explodes at runtime, the stack trace points in console log at generated LUA, not your TypeScript. The output is still easily readable (TSTL does a good job), but there’s a mental translation step.

TSTL has sharp edges. It’s a transpiler, not magic. My favourite landmine: a continue, break, or return inside a catch block compiles to an invalid cross-function goto — the Lua file fails to load with an “undefined label” error and takes the whole server down with it. Lua’s 1-based arrays are hidden from you almost everywhere, until the one place they aren’t.

You still need to know the engine. TypeScript doesn’t save you from Dota 2’s quirks — SetGoldPerTick simply doesn’t work, and damage filter sign conventions will gaslight you. Types make the API discoverable; they don’t make it sane. Ask around in ModDota discord for help.

Build pipeline complexity. There are more moving parts than a folder of .lua files: TSTL config, webpack config, gulp tasks, the Excel watcher. When it works (which is nearly always), it’s invisible. When it breaks, you need to understand the chain.

For me, every one of these is a worthwhile trade.

TL;DR:

  • x-template gives you a complete Dota 2 custom game pipeline: TypeScript game logic (via TSTL), React UI (via react-panorama), Excel-driven game data, localization, hot reload, and encrypted publishing
  • TypeScript catches the classic LUA failure modes — nil access, API typos, wrong arguments — at compile time instead of mid-playtest
  • Shared type definitions keep client and server in sync; renaming a net table field becomes a compile error, not a silent UI bug
  • Writing Panorama UI in React with hooks is dramatically nicer than imperative panel manipulation
  • Downsides exist: debugging generated Lua, occasional TSTL landmines, and a heavier build pipeline — all worth it in my experience
  • IMBA Wars is built entirely on this stack — IMBA Wars
  • A personal english fork of x-template with some flavours into it

Closing Thoughts

Writing Nexus Defense in raw LUA was such a pain, that i even try to do some progress while working nights, but that was impossible with my current slow laptop (P.S: I am waiting for Framework 13 Pro reviews). The Dota 2 modding scene has always had a brutal iteration loop — anything that moves errors from “in-game, after a loading screen” to “in the editor, before I even save” is totally worth its weight. Writing IMBA Wars in TypeScript is the best changes i have made.

Huge credit to XavierCHN for maintaining x-template, and to the ModDota community for the type definitions that make all of this possible. If you’re starting a custom game, clone the template, run yarn dev, and thank me later.

More IMBA Wars development posts coming soon — there’s a lot to say about rewriting vanilla abilities into IMBA versions, injecting abilities into towers at runtime, and getting 10v10 bot lobbies working.