| title | Caveats |
|---|
TSTL aims to support almost all modern, idiomatic TypeScript without any modifications. In other words, you probably will not have to worry about the idiomatic quirks of Lua or other internal decisions that TSTL makes when converting code.
With that said, TSTL does have some "gotchas" that you might run into. This page covers some of those edge-cases.
| Feature | Lua 5.0 | Lua 5.1 | Lua 5.2 | Lua 5.3 | LuaJIT |
|---|---|---|---|---|---|
| Missing features | ❌ | ❌ | ❌ | ❌ | ❌ |
| Bitwise operators | ❌ | ❌ | ✔️ | ✔️ | ✔️ |
continue |
❌ | ❌ | ✔️ | ✔️ | ✔️ |
| (everything else) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
This project aims for JavaScript and Lua compilation results to have the same runtime behavior as much as possible, but not at all costs. Since TypeScript is based on JavaScript, it also inherited some of the quirks in JavaScript that are not present in Lua. This is where behavior between Lua and JavaScript compilation targets diverge. TypeScriptToLua aims to keep identical behavior as long as sane TypeScript is used: if JavaScript-specific quirks are used, behavior might differ.
Below are some of the cases where resulting Lua intentionally behaves different from compiled JS.
One of TypeScript's design goals is not using type information to affect program runtime behavior. Though this has many advantages (such as gradual typing), TypeScriptToLua uses type information extensively. This allows us to emit a much more optimized, portable, and correct Lua code.
JavaScript and Lua differ in what they evaluate to true/false. TypeScriptToLua adheres to the Lua evaluations.
| TypeScript | JavaScript behavior | Lua behavior |
|---|---|---|
false |
false |
false |
undefined |
false |
false |
null |
false |
false |
NaN |
false |
true |
"" |
false |
true |
0 |
false |
true |
| (Everything else) | true |
true |
We recommend that you use the strict-boolean-expression ESLint rule in your TSTL projects, which will force you to be explicit and prevent this class of bug entirely.
TypeScriptToLua makes no difference between == and === when compiling to Lua, treating all comparisons as strict (===).
We recommend that you use the eqeqeq ESLint rule, which will force you to be explicit and prevent this class of bug entirely.
nil is the Lua equivalent for undefined, so TSTL converts undefined to nil. However, there is no Lua equivlanet for null, so TSTL converts null to nil as well.
This means that TSTL programs with null will have different behavior than JavaScript/TypeScript programs. For example:
const foo = {
someProp1: 123,
someProp2: null,
someProp3: undefined,
};If we iterated over foo in a TSTL program, we would only get someProp1, instead of both someProp1 and someProp2 like we would in a JavaScript/TypeScript program.
In general, we recommend keeping null out of your TSTL codebases in favor of undefined. Not only will this represent the transpiled Lua code better, but it is more idiomatic in TypeScript to prefer undefined over null when both would accomplish the same thing.
Array.prototype.length is translated to Lua's # operator. Due to the way arrays are implemented in Lua, there can be differences between JavaScript's myArray.length and Lua's #myArray. The transpiler does not do anything to remedy these differences. Thus, when working with arrays, the transpiled Lua will use the standard Lua conventions. Generally speaking, the situation where these differences occur happen when adding/removing items to an array in a hacky way, or when setting array items to undefined / null.
For example:
const myArray = [1, 2, 3];
myArray.push(4);
myArray.pop();
myArray.splice(1, 1);
// myArray.length == 2const myArray = [1, 2, 3];
myArray[1] = undefined;
// myArray.length == 1 (which would be 3 in JavaScript)const myArray = [1, 2, 3];
myArray[4] = 5;
// myArray.length == 3 (which would be 5 in JavaScript)Even though iterating over object keys with for ... in does not guarantee order in either JavaScript or Lua. Therefore, the iteration order in JavaScript is likely different from the order in Lua.
Note: If a specific order is required, it is better to use ordered collections like arrays instead.
Not allowed. Use a for of loop instead to iterate over an array.
A sorting algorithm is said to be stable if two objects with equal keys appear in the same order in sorted output as they appear in the input array to be sorted.
- Sorting is part of the JavaScript standard library via the
Array.sortmethod. It is guaraunteed to be stable. - Sorting is also part of the Lua standard library via the
table.sortmethod. It is not guaraunteed to be stable.
TypeScriptToLua relies on the Lua standard library for sorting. In other words, it transpiles [1, 2, 3].sort(); to table.sort({1, 2, 3}). So beware that your sorts will no longer be stable!
If you need stable sorting, you will have to implement your own sorting algorithm or find a library that provides one.
In most cases, TSTL creates Lua code that declares variables using the local keyword, which makes the variables local to the function or block. In other words:
const foo = 123;Usually gets transpiled to:
local foo = 123In JavaScript/TypeScript, there is no limit to the amount of variables that you can create. However, in Lua, there is a limit of 200 local variables at any point in time. For big TSTL programs, this can be a problem, causing a run-time error in production that the compiler will not catch!
For example, imagine that a TSTL program consists of 101 individual features that are separated out into different feature classes, each in their own separate file. And upon program startup, all of the classes are instantiated:
import { Feature1 } from "./features/Feature1";
import { Feature2 } from "./features/Feature2";
import { Feature3 } from "./features/Feature3";
...
import { Feature101 } from "./features/Feature101";
const FEATURE_CLASSES = [
Feature1,
Feature2,
Feature3,
...,
Feature101,
];
for (const featureClass of FEATURE_CLASSES) {
new featureClass();
}Since each transpiled import statement creates two separate local variables, this would create 202 local variables, and the program would immediately crash upon first being loaded.
You can solve this problem in a few different ways. For this specific pattern, we recommend using a barrel file, which is a file that contains only imports and exports. Specifically, our fixed program would look like this:
export { Feature1 } from "./features/Feature1";
export { Feature2 } from "./features/Feature1";
export { Feature3 } from "./features/Feature1";
...
export { Feature101 } from "./features/Feature101";import * as fc from "./featureClasses.ts";
const FEATURE_CLASSES = [
fc.Feature1,
fc.Feature2,
fc.Feature3,
...,
fc.Feature101,
];
for (const featureClass of FEATURE_CLASSES) {
new featureClass();
}Importatly, once we have a barrel file, we do not have to artificially split up the number of classes. This is because TSTL does not transpile exports with any local variables at all. Thus, we can have an unlimited number of exports inside of the barrel file without ever hitting the Lua local variable limit.