Skip to content

Instantly share code, notes, and snippets.

@Zamiell
Last active December 15, 2023 18:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Zamiell/26625f57a875831a936d7b053ec89f24 to your computer and use it in GitHub Desktop.
Save Zamiell/26625f57a875831a936d7b053ec89f24 to your computer and use it in GitHub Desktop.
TypeScriptToLua & File Size

Introduction

Do you use TypeScriptToLua? Do you find the transpiled Lua output to be unsatisfying in some way? Please don't care about this. This blog will attempt to convince you why.


What is TSTL?

TypeScriptToLua, or TSTL for short, is a tool to automatically convert your TypeScript code to Lua.


Why would you want to use TSTL?

Using the Lua programming language is required for a bunch of use-cases, like modding games or writing software plugins. However, the Lua language itself is absolutely terrible: it lacks type-safety and other basic features like switch statements, optional function arguments, destructuring, and more.

TypeScript has exploded in popularity, and as of 2022, most of the internet has already switched their codebases from JavaScript to TypeScript. And it's not hard to see why, as once you spend a few days with TypeScript, you will likely never go back.

For the same reasons that you would want to write your JavaScript programs in TypeScript, you will also want to write your Lua programs in TypeScript using TSTL.


Does TSTL make "clean" Lua code?

That depends on your definition of "clean".

Let's start by showing an example of a simple function that might be used in a video game mod:

/** Triggered whenever a hero casts the spell. */
function onAbilityCast(this: void, caster: Unit, targetLocation: Vector) {
    const units = findUnitsInRadius(targetLocation, 500);
    const enemies = units.filter(unit => caster.isEnemy(unit));

    for (const enemy of enemies) {
        enemy.kill();
    }
}

This transpiles to:

--- Triggered whenever a hero casts the spell.
function onAbilityCast(caster, targetLocation)
    local units = findUnitsInRadius(targetLocation, 500)
    local enemies = __TS__ArrayFilter(
        units,
        function(____, unit) return caster:isEnemy(unit) end
    )
    for ____, enemy in ipairs(enemies) do
        enemy:kill()
    end
end

As we can see, this is very similar to the original code and still quite easy to read! All of the JSDoc-style comments are transferred over as-is. And all of the variable names remain intact.

However, a real world TypeScript program is probably going to have switch statements, optional chaining operators (i.e. ?), and other advanced language features. These kinds of things do not have one-to-one Lua correspondants, so they have to be implemented with if statements and other kinds of verbose boilerplate.

For this reason, TSTL programs are not going to be as visually appealing or easy to read as hand-crafted Lua programs. On average, they will probably be 1.5x the length of the source code.


Does it matter if the transpiled code is messy?

The target audience for TSTL is Lua programmers who are looking for an upgrade. But Lua programmers have beautiful-looking Lua code. So when they rewrite a function in TypeScript, and then compare their original code to the nasty looking Lua code that is generated from the machine, they recoil. They notice that it is harder to read the verbose machine-written code than it is to read their own hand-crafted code. So they consider this to be a disadvantage of the tool.

Of course, this logic only makes sense when your brain is thinking in Lua-mode. Once you start to think in TypeScript, it doesn't make a whiff of a difference what the transpiled output looks like, because all you are doing is reading the TypeScript code. Lua is not the source code, TypeScript is, and all that matters is the source code.

As a quick analogy, let's consider the case of C++ code. People write code in the "high-level" language of C++, which looks like this:

#include <iostream>

int main() {
    std::cout << "Hello, World";
    return 0;
}

Then, when they run the C++ code through a compiler, it produces X86/X64 code, which looks like this:

          global    _start

          section   .text
_start:   mov       rax, 1
          mov       rdi, 1
          mov       rsi, message
          mov       rdx, 13
          syscall
          mov       rax, 60
          xor       rdi, rdi
          syscall

          section   .data
message:  db        "Hello, World", 10

As we can see, the compiled output doesn't really look much like what we started with. Rather, it is a lower level version of the code, which is naturally going to be more verbose.

For nearly all intents and purposes, we don't really care what the X86 code looks like, because we should never need to look at it. All we care about is the source code in C++. The source code is what we read to understand how the program works. The source code is the thing that gets checked into GitHub. The source code is the thing that is going to be read by other programmers on the team.

Imagine that someone came along and told you that "a disadvantage of coding in C++ is that the compiled output is really hard to read". This would probably strike you as being quite strange. What point is there in reading the X86 code? It might as well be written in Klingon instead of X86 for all we care, since we never have to deal with it.

The situation with C++ and X86 is very similar to the situation with TypeScript and Lua. In other words, the fact that Lua is "unreadable" is about as relevant as the fact that X86 is unreadable.


But won't there be situations where I will need to look at the transpiled code?

The big caveat to the previous section is that when run-time errors occur, the Lua engine will tell you the line of code that the error happened on. This means that TSTL coders might have to jump into the transpiled file from time to time to find out what went wrong (by looking at the corresponding line). So, if the Lua file is a mess, then troubleshooting run-time errors could be really difficult.

However, in practice, troubleshooting run-time errors is not a problem because the generated Lua code is very close to the TypeScript code (see above). It has all of the same variable names, function names, and so on. For any given line of Lua code, it is pretty trivial to Ctrl + Shift + F in your project and find the corresponding line of TypeScript code.

It's also important to remember that run-time errors are very rare in TSTL land - that's the whole point of using TypeScript in the first place!


Does the file size of the transpiled output matter?

If you chose to write your program in TypeScript instead of Lua, the file size of the final product will be bigger. But for most applications, this will not matter. For example, consider a game mod or plugin: the end-user's hard drive would read both a 2 megabyte file and a 20 megabyte file in a fraction of a section. They would never notice even an order of magnitude difference.

Even if you have a 20+ megabyte transpiled Lua file, this is almost certainly going to be eclipsed by the size of your graphics, sound effects, or other resources included with your program. So it makes more sense focusing on compressing or replacing those things rather than micro-manging the size of the code.

If you really, really care about reducing the file size of your TSTL program, then you could just run it through a Lua minifier, which will probably reduce the size of the output by around a factor of 2. (But minification makes troubleshooting run-time errors more difficult, so by default you should just not care about this.)


Is hand-crafted Lua code more performant than TSTL code?

Obviously, there will be certain specific cases in which hand-written code is more optimized than code generated by a computer. But in almost every case, the actual algorithms that you are using in your code (e.g. O(N^2) versus O(N)) is the thing that will affect the run-time performance of your code, not an extra superfluous if statement generated by TSTL. So this kind of thing is not worth worrying about.

Modern computers can call a function or go through an if statement in mere nanoseconds. Here, it is useful to remember the three rules of optimization. Essentially, we should focus on identifying the actual bottlenecks in our code and measuring the real-world impact when tinkering with them, as opposed to obsessing over theoretical nanosecond-level improvements that are too small to even measure.


Conclusion

If you think the TSTL output is unsightly, verbose, or produces unnecessary output, this almost certainly does not matter:

  • The source code is what matters, not the transpiled output.
  • Troubleshooting run-time errors from transpiled code is trivial.
  • Outside of that, no-one should ever look at the Lua code; they should look at the TypeScript code instead.
  • The marginally increased file size is too small to matter.
  • TSTL programs will not run slower than their counterparts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment