Dev Thicket

Why I Built a New Programming Language (and Why You Might Not Need It)

I love TypeScript. I just wished it could compile to a C++ VM, had structs on the stack, and did not bring JavaScript along for the ride. So I built ThicketScript.

I love TypeScript. I have written it professionally for years. The type system is expressive and the syntax gives you types, definitions, and usages without guessing. When I started building a C++ game engine and needed a scripting layer on top, TypeScript's syntax was the only thing I wanted to write in.

But TypeScript is not a standalone language. It compiles to JavaScript. JavaScript means V8. And V8, for all its brilliance, is not built to live inside a game engine. I did not want to give up the syntax I loved just because the runtime underneath did not fit. So I built ThicketScript: the TypeScript-like syntax I wanted, compiled to bytecode for a C++ VM I controlled. No JavaScript. No V8. No compromises.

I know what you are thinking. Before you close this tab, hear me out on the objections.

"Just use Lua"

Lua is great. Battle-tested, tiny, fast, embeddable. Millions of games ship with it. If Lua works for your project, use it. Genuinely.

Here is where it did not work for me:

  • No static types. Optional type annotations exist through tools like Teal or LuaLS, but they are bolted on. When your game has hundreds of scripts, catching type errors at compile time instead of at runtime saves real debugging time.
  • 1-indexed arrays. Every C++ developer who touches Lua makes an off-by-one error in the first week. Every time. It never stops being annoying.
  • Tables for everything. Lua's single data structure is elegant in theory and messy in practice. When a table is simultaneously an array, a dictionary, and an object, you lose the ability to reason about what you are looking at.
  • Weak IDE support. Lua tooling has improved, but it is nowhere near what you get with a statically-typed language and dedicated IDE plugins. Autocomplete, go-to-definition, and inline errors matter when you are writing game logic all day.
  • The syntax is not what I want to write in. This is subjective and I own that. But if I am going to spend years writing game logic, UI code, and tools in a language, I want it to feel like home. TypeScript syntax feels like home. Lua does not.

Lua earned its place in the industry. ThicketScript is not a replacement for Lua. It is an alternative for people who want different tradeoffs.

"Just use actual TypeScript"

This is the question I asked myself first. I already knew the syntax. I already had the muscle memory. Why not just embed V8 and call it a day?

  • Huge binary size. V8 adds tens of megabytes to your application. For a game that ships as a download, that matters.
  • JIT warmup. V8's performance comes from JIT compilation, which takes time to kick in. Game scripts that run for a few frames never get fast.
  • Unpredictable GC pauses. V8's garbage collector is optimized for throughput, not latency. At 60fps, you have 16ms per frame. A GC pause longer than that is a dropped frame and a visible stutter.
  • Complex embedding. V8's embedding API is powerful and complicated. Isolates, contexts, handles, scopes. Registering a native function is a project in itself.
  • JavaScript baggage. Even with TypeScript on top, the runtime is still JavaScript. Prototype chains, implicit coercion, this rebinding, == vs ===. TypeScript catches some of this at compile time, but the runtime does not care about your type annotations.

I also wanted things TypeScript cannot give me. Structs that live on the stack instead of the heap. Numeric types smaller than 64-bit. A permission system baked into the compiler. Control over when garbage collection runs. These are not TypeScript limitations; they are JavaScript runtime limitations. The only way to get them was to build a new runtime.

"I'll just write everything in C++/C#/Go"

If you are writing the entire game in one compiled language and shipping it as a binary, you probably do not need a scripting language. That is a valid choice and many great games ship this way.

You start wanting a scripting layer when:

  • You want modding support. Letting players run C++ in your process is not an option. You need a sandboxed language with permission controls. ThicketScript starts with zero capabilities; the host opts in to what scripts can access.
  • You want hot reload. Recompiling C++ to test a dialog change or tweak an AI behavior is slow. ThicketScript files reload without restarting the host application.
  • You want data-driven architecture. Scripts are easier to manage with tooling than compiled code that needs build configurations per platform. Store scripts in a database, ship them to the server, update behavior without redeploying binaries.
  • You want server-side isolation. If scripts live in a database and run on the server, you need sandboxed execution with per-context permissions and resource limits. A compiled language running in-process does not give you that. A VM does.
  • You want to separate concerns. Engine code changes rarely and needs to be rock-solid. Game logic changes constantly and needs fast iteration. Different stability requirements, different tools.

If none of those apply, keep doing what you are doing. ThicketScript exists for projects where a scripting layer makes the workflow better.

What I actually built

ThicketScript takes the TypeScript syntax I love and compiles it to bytecode for a purpose-built C++ VM. It has its own compiler, its own runtime, its own .tks file extension, and dedicated IDE plugins for JetBrains and VS Code. About 80% of TypeScript syntax works in ThicketScript as-is. The other 20% is where it gets interesting.

Things I wished TypeScript had

  • First-class structs. Value types that live on the stack. No heap allocation, no garbage collection. Define methods and operator overloads directly on the struct. A Vec2 is 8 bytes, not a heap object wrapped in a property map.
  • Compact numeric types. int8, uint16, float32, and the rest. A Color with four uint8 fields is 4 bytes. TypeScript has one number type for everything because JavaScript has one number type for everything.
  • Series and parallel. Function modifiers or scope blocks for async control flow. series function awaits every call automatically. parallel function runs everything at once. No more Promise.all wrappers or await on every single line.
  • Client blocks. In multiplayer games, a client block sends code from server to client with automatic variable capture. The compiler verifies the boundary. This is not something you can bolt on with a library.
  • Capability-based security. Scripts start with zero permissions. The host grants specific capabilities: filesystem read, network access, crypto. The compiler catches permission violations before runtime. Essential for modding and plugin systems.

Things I did not need

Conditional types, mapped types, template literal types, eval, Proxy, Reflect, prototype chains, this rebinding, and implicit coercion. These serve library authors writing framework-level type gymnastics. Game developers writing game logic do not need them, and removing them makes the language simpler, faster, and easier to reason about.

IDE support is non-negotiable

A language without tooling is a language nobody wants to write. One of the things I love most about TypeScript is the IDE experience. ThicketScript had to match it or there was no point.

ThicketScript ships with native language plugins for JetBrains IDEs and VS Code. Not syntax highlighting hacks. Real plugins with autocomplete, go-to-definition, inline error highlighting, and type checking as you type. Both plugins ship when the language ships. If the tooling is not good on day one, the language does not deserve to exist.

Built to embed

ThicketScript is a library, not a platform. Link it into your C++ application, register your functions, and run scripts. The embedding API is intentionally small:

  • define_native() to register C++ functions that scripts can call.
  • define_global() to expose C++ objects to the script environment.
  • execute() to run a script.
  • step_instructions(budget) to run a bounded number of instructions per frame, so scripts never block your game loop.
  • gc_step(budget_us) to control when garbage collection runs and for how long.

No isolates. No bridge layer. No serialization. The VM sits inside your application and stays out of the way.

Open source

The ThicketScript runtime; the VM, compiler, parser, type checker, garbage collector, standard library, and embedder API; will be open source and free for both personal and commercial use. I built this because I needed it. If it is useful to you, even better.

Who this is for

If you are building a C++ application that needs scripting; a game engine, an editor, a simulation, a tool; and you want TypeScript-like syntax, static types, real IDE support, and an embedding API that respects your architecture, ThicketScript might be worth a look.

If Lua works for you, keep using Lua. If you are happy in C++, stay there. I am not here to convince anyone to switch for the sake of switching. I built ThicketScript because the existing options did not fit what I needed. They were all close, but none of them were right. If you have felt the same way, you might understand why I built it.

Learn more about ThicketScript, or check out the FAQ for technical details.