blog.oat.zone
λ
cd /isaacscript-and-why-it-sucks/

IsaacScript, and why it sucks

· by jill

You may have heard people in the Isaac modding scene use IsaacScript, a TypeScript drop-in for Isaac modding. But what is it exactly, and is it any good, or is it just a meme?


This article is out of date. I have published a revised version and kept this one up for archival purposes. Reading this article is not required to read the new one.

If you've ever been around the Isaac modding scene, you've likely heard of IsaacScript. It's been around for a while now, ever since AB+, the inception of the modding API. It describes itself as this:

IsaacScript is a tool to help you create Binding of Isaac: Repentance mods using TypeScript.

That's about all there is to it. It uses the typescript-to-lua transpiler (a piece of software that turns code from one language into code for another language) to turn your TypeScript code into Lua (since that's what Isaac's modding API uses). In fact, aside from this, it also includes abunch of utilities to make Isaac modding easier!

And, to an outsider or a beginner in making Isaac mods, this sounds great! Why not just, use IsaacScript and a better language than Lua to code up some mods, right?

Yeah, no. This post isn't gonna say much positive things about this project. I have some.. complaints with it, its core premise and its creator.

Just to be completely clear - I don't hate you if you use IsaacScript. I don't hate anyone for using it. If it's comfortable for you, then who cares. I'm here to provide good reasoning for why a beginner shouldn't approach it and instantly decide to use it, as this can often make Isaac modding seem much harder than it really is.

The first and most obvious complaint is, well, it's TypeScript. TypeScript isn't a good language. Even if TypeScript is better than Lua, it's very much not worth the overhead of using a compiled language in place of an interpreted one, and all of the IsaacScript issues. (Which, I'll get to soon, don't worry. It's homepage does a very good job of making it seem like everything ✨ just works ✨, but it doesn't.)

Let's start with the problems that come up right away, just thinking about this project.


On Paper

TypeScript sucks.

Listen, this isn't a new thought that I've come up with on the spot. This has been said and proven multiple times now.

Let's start with a quick history of how TypeScript came to be. This should better explain why it sucks.

As the web was created, JavaScript quickly emerged. It's original prototype, what defined the syntax, what defined it's nowadays quirks, was made in only a few weeks.

JavaScript, a.k.a. Mocha, was born in this context. In a matter of weeks a working prototype was functional, and so it was integrated into Netscape Communicator.

from A Brief History of JavaScript

This may sound like not that bad of a thing. So what if the initial prototype was quickly made? You can just, update JavaScript, right? Well, that isn't quite how the web functions.

Once you have a function or some sort of syntax set in place, you can't change it. Else? You break sites that already use this functionality. If one site was to use, say, for (let i in array) {}, then if you were to change the syntax, that site would break and all of its users wouldn't like that very much.

As JavaScript doesn't have any versioning or anything of the sort, this is bad. This would've been fine if the language was designed fine from the start; but again, it was designed "in a matter of weeks".

Because of that, we get jank like ===, for (let o of array), var, 'use strict', etc... If you've ever written JavaScript software or websites, you'll know how awful it is to deal with. Janky solutions on top of janky solutions, instead of making a new language or doing anything else.

And this is just the surface level issues; this is ignoring performance issues, engine-level issues, and everything else. JavaScript isn't a good language.

Now, you may think. "But TypeScript isn't JavaScript! It's main purpose is improving on JavaScript"! But this is only partly true, as TS aims to be a drop-in replacement for most JS code. It only fixes the typing, and everything else is still the same. So the === criticism still applies, for example.

It's still an improvement, but again, it's made to work for the web. It's primary purpose is to make JavaScript developers who are forced to use JS for the web suffer a lot less. You have to build it, sure, but in return your code becomes a lot more safe and a lot less [object Object] and undefined.

This is true for anything on the web (at least, in my opinion), but when you take JavaScript, something that's bad because of how the web was created and drag it outside of the web, it's not gonna be a good idea. Even if you take TypeScript, it's still made to work for the web, specifically JavaScript, and it's not gonna be pretty.

Now, to be fair, IsaacScript doesn't exactly use TypeScript. It only uses it's syntax as it's transpiled, and I've heard == aliases to === in it. But that's only a patchy, temporary solution - instead of taking TypeScript and applying patches after patches to it to make it better for using outside of the web and using it outside of having the code be backwards compatible, why not.. use a different language?

Don't get me wrong, choosing a language isn't easy. I know exactly why TypeScript was chosen - there was already a neat TypeScript to Lua transpiler out there, and it'd be easier to just take that than to design your own transpiler or even worse, your own language. But in the end, you'd want to aim for a more long-term solution, one less janky than what TypeScript is.

For example, there exist MoonScript, Teal, Fennel, Amulet, Haxe (and more!) - all languages that have Lua compilation as a direct target of compilation. They are all designed to compile to Lua, unlike TS2L.

(While it's true all 6 of these (including TS2L) compile to an AST, from which they're able to compile to Lua, I'm referring to their core structural design - the syntax, the intented functionality of standard library functions, etc.)

Why not, instead, heavily modify TypeScript's syntax to be more fit for IsaacScript? IsaacScript is already named in a way that misleads you into thinking it might be another new language for Isaac modding in specific, so why not go the full mile?

As an added bonus, you could completely abandon the TS2L transpiler, and instead have Lua compiling be the main goal of the project. That way you'll avoid lots of issues that arise from TypeScript being shoved in Isaac modding (which, we will get to in a moment).

But, okay. Enough JavaScript bashing. What about everything else?

Let's talk about the "gotcha"s

Of course, embedding the entirety of TypeScript into your Lua mods isn't going to come at zero cost. And luckily all issues that arise from this, at least on the developer end, are neatly documented! Let's take a look.

If you read through it, you'll notice it's not that long, but it's still quite a bit to consider that's neatly tucked away in the wiki. (And believe me, there are going to be more gotchas in the "In Practice" section.)

For example, the Vector() class that Isaac gives all modders access to and forces you to use in certain cases is basically handicapped - where before you could simply add two vectors together with vec1 + vec2, TypeScript, unsurprisingly, isn't designed for the use case of Isaac modding, doesn't have operator overloading, and will yell at you. The proposed alternative?

// TypeScript code
let vector = Vector(3, 3).mul(6).add(Vector(1, 1);
Not so pretty anymore, huh?

Yeah, it's not the best. But surely we could work around this and all the other gotchas just fine, right?

(I want to note I've been told this is possible, but it "destroys type safety". What this means I have no clue, but I assume that isn't something you'd want out of a typed language, of all things.)

The overhead of a compiler

I want to make it incredibly clear that a compiler isn't something you should just shove at any point willy-nilly. While it's easy to take it as such, a compiler actually does quite a lot of stuff and changes quite a lot of stuff about your work environment.

While IsaacScript does simplify this down quite a bit, it still doesn't completely fix every issue with compiled, especially transpiled code, but it markets itself as such anyways.

If you want to build code, you can no longer download a zip of the source code and shove it in your mods folder. You have to set up an environment, download IsaacScript, get that working and then compile it. While during work you can turn on the watch script and not worry about it, the initial setup is still there, and is even longer for new projects.

While my proposed solutions for IsaacScript alternatives still require set up, you can always, at any point, download the current source code of the project, shove it into mods, and it'll just work. No code needs to be built, nothing. It's important to consider this for when you consider adding a compiler to your work environment, as then making any contributions to the code will require building, since to test the code you need to, well, build the code.

Transpiling TypeScript -> Lua (and transpiling in general) also isn't completely risk-free. The TypeScript transpiler the project uses isn't exactly stable (from personal experience) and will tend to sometimes get stuff wrong. It also will over-complicate code, make code that's often hard to read, lots of things like this (and we will get to this later!). It's easy to ignore it all to an extent, but you shouldn't ever forget what IsaacScript actually does internally.

The way it's branded

This is pretty petty, I'll admit. It has nothing to do with the functionality, ideas or IsaacScript in practice, but it has to be addressed. Let's just, take a quick look at the website, since it's very often advertised in place of anything else. (The GitHub page README is basically just, "go to the website"...)

The page that interests us in specific is this one, labelled "Features".

The Features tab highlighted in the IsaacScript website.

Truly, if IsaacScript is so great, the features should be enough to get anyone hooked on the idea. They're the easiest to pick apart and look through, right?

The entire Isaac API, strongly typed - code fearlessly without having to worry about making a typo or having to look at the docs.
Mouseover API calls to see what they do what parameters they expect. Hopefully, you will never have to open the Isaac documentation ever again. Good riddance.
(I grouped these two as one as they're practically the same.)

Right. Starting off strong with 2 points that can be done just as well without a TS transpiler.

This.. is something that already exists. The unofficial Isaac docs have pretty specific types - even specifying when you can pass in a floating point number, and when you can pass in an integer. (This is something you actually can't do in TypeScript fluently, and is mentioned in the "gotchas" section of the website.)

This specifically adds functionality for IDEs and code editors to use hover-on features, but I.. hate to be the one that breaks this to you, but this doesn't depend on the language.

This depends on the language server of your language, which plenty exist of for Lua. For example, here's me avoiding a typo this way!

The code defines a table called "test", and puts in a function called "testfunction". Upon referencing the function later in the code, I'm able to see the type signature and the comment I put above the function.
If you wanted to, you can define the x variable's type if you were to use LDoc or similar.
Mouseover documentation, too!

Alternatively, there's also Kailua - it describes itself as "an experimental type checker and integrated development environment (IDE) for the Lua programming language". It's somewhat of a combination of a linter and a language server.

So, in short, this is an excessive solution to a problem that never existed.

Better API Accuracy - The Isaac documentation is wrong in a lot of places. Some functions are not implemented and some functions make the game crash. Don't bother waiting for a patch - the isaacscript framework fixes everything for you.

This.. is an odd point. If a function isn't implemented or crashes, it's most often documented, and you'll know if you look for even half a second for what the function does. And if you use a language server, the solution offered above, it's going to be as fluent as what IsaacScript offers.

If you don't like how the Isaac docs document something? They're completely open source. Submit a PR if it bothers you, and everyone will benefit from it, rather than investing in an IsaacScript version of it, that only a subset of users will be able to use.

The Isaac API mentions that running this function with invalid arguments will make the game crash. This is highlighted in red.
I'm so disappointed there isn't a clear warning saying this function can crash the game..

IsaacScript feels like an odd solution to this - why create a whole TypeScript transpiler just for this? I guess I'd understand this as a bonus point, but it's not. It's one of the top points listed as features.

And, sure – IsaacScript does patch up a few API functions to work better, and while this would feel more native with IsaacScript as you have the opportunity to integrate it into the compiler itself, it's still just as possible with Lua.

And, to be fair, do you really need to have something solve things like this? All you need to be aware of is what issues can arise, and then see how those can arise with your inputs into the functions, sanitize them, and move on. But to be honest, this is almost entirely opinionated - some would just prefer it if everything just, worked, and using the tools provided is completely fair.

Though, I will be honest - it's not very good to design IsaacScript as a monolithic Swiss-knife Isaac modding toolkit that handles everything for you.

Automatic Mod Reloading - Never close and reopen your game again when working on your mods. Never type the luamod console command again. If you use include to get around bugs with luamod and require, don't bother - that isn't needed here.

I'll be honest - this comes down to preference. I'd rather prefer that I reload the mod manually, as I don't want the game to run potentially broken code.

I save very frequently (mainly out of paranoia), and if I were to, say, leave a while loop improperly closed, it'd infloop the game and force me to restart it. However, it's completely fine if you prefer it this way! (Again, you don't need TypeScript to do this, but still...)

What isn't completely fine is that last line. It bugs me quite a bit.

If you use include() to get around bugs with luamod and require(), don't bother - that isn't needed here.

For context, Lua has a function called require. All it does is load a file, and run the entirety of it as a function. Whatever it returns is what require returns. It's really handy for code modulation, and is practically your only choice aside from dofile.

In Isaac, as of Afterbirth+, require doesn't work as expected - Lua caches all files that you require, meaning you need to restart the game for your code to update. This really sucked, and the only workaround we had at the time was to either:

  1. Shove everything in main.lua or create a build script that does this,
  2. Use dofile hacks, coined by piber, or
  3. Suffer.

However, once the Repentance DLC rolled around, we now have access to include. It functions identically to require, but doesn't do the weird caching.

This comes at no further consequence. You replace each require with include, it works, and you never look back. It's just a small quirk of Isaac modding you get accustomed to, just like lots of what working with the Isaac modding API is like.

(While I know include has issues with --luadebug, I'll be completely honest - it shouldn't be the modder's burden to care about Isaac's API breaking specific usecases of the modding API.)

(Update: I've now realized you can solve that too by simply putting include = include or require at the top of your main.lua.)

(Update: This has now been fixed in a patch. This point is now entirely redundant.)

Extra Enums - Seamlessly utilize community-contributed enums for things that the developers forgot to include in the enums.lua file.

To be fair - this one does sound neat. The community-contributed enums are actually ones that are useful in code - for example, entity variants that have mostly gone undocumented.

Though, again, this could all be done with a language server, or even just plain code editor built-in IntelliSense.

Never waste time formatting a file again. Automatic file formatting with Prettier comes running out-of-the-box.

Formatting a file isn't a "waste of time", it's something you need to integrate into your coding style, it's something you need to get accustomed to and something that's a crucial part of learning how to code. Though this is an opinionated take, and I will admit in some cases you'd prefer to actually use something like Prettier to help you with this. You're completely okay if you choose to use Prettier over manual code formatting.

Besides, you can still do this with Lua. Here's two tools that do this for you! And here's a VSCode extension that integrates this into your editor! And, if you're not happy yet, here's a Prettier plugin that integrates Lua into Prettier. My point is, I've never done it myself as I'd rather not depend on an external tool to do formatting work for me, but I'm sure it's easy enough to figure out on your own.

Squash all the bugs and ensure code consistency with the world's best linter, ESLint. It comes running out-of-the-box.

Hey, check this out.

This works really well. I've used in group modding projects, and it ensures everyone on the team knows what the coding style is in the repository, and it ensures it stays consistent.

In fact, I'm nice enough to make the .luacheckrc I use for Isaac modding public!

Feel free to use it. It includes most globals you'll use in Isaac mods, so you don't accidentally typo one of them or use one you think exists, but doesn't. Your editor will (hopefully) give you an explicit warning about it. Hey, this is starting to sound familiar...

TypeScript - Enjoy all the benefits of a strongly typed language over the shit-show that is Lua

Okay. Oh no. Oh no.

I've addressed how TypeScript sucks before, but most arguments you throw at the creator will result in "well, IsaacScript transpiles to Lua, and not JavaScript, so it's fine", and as I've addressed before, that's not exactly true as TypeScript is still designed to work with JavaScript.

While you can throw out engine and performance-related reasons out the window because of this, the syntax and design of the syntax still stands, and even that's not very good.

And even ignoring that, TypeScript is anything but strongly typed - if anything, anything in your code doesn't provide types correctly (which can even be standard library functions), you either manually define them (which is going to be a hassle to end-users), or just leave it as any. One trace of any, and your code becomes unsafe.

Don't get me wrong, TypeScript on it's own, as a concept, is pretty strongly typed. While any exists, it doesn't make a language bad - what makes TypeScript's typing bad and not as strong as it claims is how often you'll have to pick between using any, making your code unsafe, or invest time into learning how declaration files work and making them. Because, remember, it's still meant to compile to JavaScript, and not everything on the web uses TypeScript and not every library out there uses TypeScript (or even has TS declaration files).

You may disagree, but honestly? I'd much prefer Lua over TypeScript in Isaac modding. While you don't exactly have the type safety of TypeScript, Lua's lack of types isn't too awful in Isaac modding, and while Isaac's modding API was designed to work with Lua, TypeScript can only achieve close-to nativeness with lots of patches upon patches.

To reiterate, you can solve Lua's bad typing with a linter and a language server. You don't work with other people's code too often in modding (besides the modding API and some uncommon APIs like EID, MCM, etc.), so you don't have to worry about others' code being typed. (And either way, even if you did depend on other's code, it's still compiled to Lua. With no .d.ts files most of the time!)


Okay, so we're done with the features part of the page. That was a nightmare and a half, and most of these issues could be solved with just "instead of using this tool meant for TypeScript, use this tool, the one that's meant for Lua, not TypeScript".

(There were more features added after this was written, but I couldn't bother picking those apart. However, I may update this article later, as I do have complaints with them as well - mainly the monolithic design pattern complaint.)

While it's true that the Lua tools are often less developed because of Lua being a less popular language, they're still pretty good. You don't need to shove TypeScript into Isaac modding to fix these, and it certainly isn't worth it, no matter how easy you make it out to be.

The cocky "IsaacScript is always better than Lua" attitude that this website (and the creator shares with the website) certainly doesn't help. It's very off-putting, actually, but I'll address this more under the "The Creator" section in the "In Practice" part.

Speaking of which, let's move on to how well IsaacScript works in practice. Surely all the issues we had on paper are just minor ones, and in reality it's much easier to work with IsaacScript than setting up dozens of Lua-related tools, with no overhead as an expense because of it, right?

..Right?


In Practice

Okay, well, sure, it may not look great on paper, but surely if I try it I'll understand just how much easier it is to setup, and that there's no overhead at all, right?

That's what I was told when I expressed my complaints to the creator. They told me that really, why am I complaining if I've never tried to even use IsaacScript? And, that's a fair point, so I decided to give it a shot.

So, how well did that go?

My attempts at running IsaacScript

I want to note the issues I've encountered have been kindly fixed by Kyojo, however I'm keeping this section up for archival purposes. You'll likely never encounter what I'm going through even on a similar setup like mine.

So, for context, Isaac pre-Repentance stored its mods in ~/.local/share/the binding of isaac afterbirth+ mods/. This is where I used to store my mods too, before Repentance rolled around. Repentance instead stores the mods in the game folder, in a convenient mods folder.

When I was getting around to switching to Repentance full-on, and the modding API stabilized, I decided to symlink the mods folder inside the game folder to the old folder. This worked really well, and everything works with it. Besides, well, you'll see..

I've kept using this symlink setup because it's really much quicker to use this over than going to Steam, then to Isaac, and then into the mods folder. I've gotten used to it, and many tools now use the old path.

So, I go to set up IsaacScript, until, oh! I'm met with an error.

IsaacScript doesn't like my mods folder.

Huh. That's odd. It says the directory isn't there. It's clearly there, as I can navigate to that exact path. But sure, let's try typing it in manually..

Entering the mods directory again, it says it's not a directory.

Ah. I see. I see. It doesn't recognize my symlink as a valid directory. Okay, well, I guess I'll just, make it use the path which the symlink links to...

Entering in the directory, it says the directory must end with "mods".

OH GREAT! There's apparently measures in place to prevent you from accidentally typing in the wrong folder name! Fuck it, let's create a temporary folder in ~/mods/ and tell it to use that folder. Surely, it's just there to put the mod inside, right?

So I go on and try to compile a mod with npx isaacscript publish. Until...

It is trying to.. delete my mod??

Fucking awesome.

Fucking awesome.

Apparently I can't use that either, for whatever mysterious reason! It definitely isn't me putting my project outside of the mods folder, as IsaacScript actually requires you to do this, which is fine - it just straight up refuses to work, for some weird reason.

The good part about any compiled language is that you can always follow the standard git clone, install the dependencies, followed by an optional cmake or configure, after which you just run make, ninja, gradle, npx tsc, whatever it is, it just works.

But no. IsaacScript is special. This is probably as a result of me just trying to compile it right away, before first using npx isaacscript on its own, and letting it watch the mods folder.

So, okay. I decide to just, symlink ALL of my mod folders towards ~/mods. Surely, that'd work? It's not a symlink, and it's a path ending in mods! And, it does work, until you try to run the IsaacScript watcher:

It's trying to create something in "data", a folder which is stored in the same folder as "mods" on a standard Isaac install.

So, wait, what was the point of even making me specify the mods folder if you're just gonna use folders outside of it, assuming the folder is inside the Isaac game folder??

Okay, sure. Fuck it, let's completely remove my setup. Let's not make the mods folder inside of the Isaac game folder a symlink. Let's move everything there, painstakingly update every symlink...

And it works! It truly does compile, after so much debugging on a setup I'd expect IsaacScript to just, handle. (Though, no offense - I know the user base is small and thus getting issues like this isn't very common, however it's still a valid criticism of IS as a whole.) (And also, this has been fixed. Sorry.)

Let's, uh, let's check what it compiled.

src/main.ts

export default function main(): void {
  // Instantiate a new mod object, which grants the ability to add callback functions that
  // correspond to in-game events
  const mod = RegisterMod("isaacscriptTesting", 1);

  // Set a callback function that corresponds to when a new run is started
  mod.AddCallback(ModCallbacks.MC_POST_GAME_STARTED, postGameStarted);

  // Print an initialization message to the "log.txt" file
  Isaac.DebugString("isaacscript-testing initialized.");
}

function postGameStarted() {
  Isaac.DebugString("Callback triggered: MC_POST_GAME_STARTED");
}

main.lua

--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]

local ____modules = {}
local ____moduleCache = {}
local ____originalRequire = require
local function require(file, ...)
    if ____moduleCache[file] then
        return ____moduleCache[file].value
    end
    if ____modules[file] then
        local module = ____modules[file]
        ____moduleCache[file] = { value = (select("#", ...) > 0) and module(...) or module(file) }
        return ____moduleCache[file].value
    else
        if ____originalRequire then
            return ____originalRequire(file)
        else
            error("module '" .. file .. "' not found")
        end
    end
end
____modules = {
["main"] = function(...) 
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
local postGameStarted
function postGameStarted(self)
    Isaac.DebugString("Callback triggered: MC_POST_GAME_STARTED")
end
function ____exports.default(self)
    local mod = RegisterMod("isaacscriptTesting", 1)
    mod:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, postGameStarted)
    Isaac.DebugString("isaacscript-testing initialized.")
end
return ____exports
 end,
["bundleEntry"] = function(...) 
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
local ____exports = {}
local ____main = require("main")
local main = ____main.default
main(nil)
return ____exports
 end,
}
return require("bundleEntry", ...)

Oh no.

So, one thing you'll notice right away - the TypeScript comments are all completely replaced with this, very, very helpful string of code:

--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]

So, if you want to do any debugging with the code, figure out why something crashes, or why something errors, you're going to need to dig through this mess with no comments whatsoever. Which is going to be extremely fun.

While you can do this exact thing with prints, it's not quite the same - if you put down comments in your code, and have an issue happen, you can trace back the comments and debug it easily. If you run into an error and have to place down prints, then that's extra effort, and you're going to have to clean it up afterwards aswell. (However, this is opinionated.)

It's easy to say "well, TypeScript is type safe so no errors could ever occur in-game", but this is a very naive thought. In a language like TypeScript, it's foolish to think it's going to prevent all errors. Your goal should instead be to make debugging possible, and making errors happen less often in development should be a secondary goal.

My point is, comments are necessary in the transpiled code. By excluding them you're making both your development efforts and the efforts of others reading your code to understand how it works much harder.

But, okay. What about the functionality of the code? Let's use an example of IsaacScript usage that's listed in the website itself - using an Isaac API function, and then applying the convenience of .filter and .map to it:

const p = Isaac.FindByType(EntityType.ENTITY_PLAYER).map(p => p.ToPlayer()).filter(p => p && !p.IsCoopGhost())
p.forEach(player => {
  player.AddKeys(10);
});

This is actually helpful - find every player, only use the ones that aren't co-op ghosts, and then give them all 10 keys. Easy, right?

Except this doesn't compile.

Object is possibly 'undefined'.

The reason this is happening is because ToPlayer() can return nil. Not every entity is a player, and therefore not every entity can be cast to a EntityPlayer.

Except, here's the thing. I explicitly only find entities that are of EntityPlayer type, as I'm only getting players, and even then I explicitly filter out undefined because of the p && in the filter.

So, okay. Let's then change that first line so that TypeScript knows for sure that it's fine, and that we're accessing things that exist:

const p = Isaac.FindByType(EntityType.ENTITY_PLAYER).map(p => p.ToPlayer()).filter(p => p && !p.IsCoopGhost()) as EntityPlayer[];

Casting in TypeScript, the as keyword, is unsafe. It's almost the same as defining a variable's type as any, as it completely disregards all typing and turns TypeScript back into regular JavaScript, functionally. So that's just wonderful. But the code does compile now!

ADDENDUM: I have been informed that using TS ? syntax you can simple this down to:

const p = Isaac.FindByType(EntityType.ENTITY_PLAYER).map(p => p.ToPlayer()?).filter(p => p && !p.IsCoopGhost());

This is okay, but the ? operator does the same as p &&, doesn't it? If p.ToPlayer() is a truthy value, it returns it, else it returns undefined, which is later filtered out anyways. I.. have no clue why my original code didn't work, I'll be honest.

Before we check the resulting code, let's see how you'd implement it in Lua:

local p = Isaac.FindByType(EntityType.ENTITY_PLAYER);
for _, player in ipairs(p) do
  p = p:ToPlayer();
  if not p then goto continue end
  if p:IsCoopGhost() then goto continue end

  p:AddKeys(10)

  ::continue::
end

This isn't too bad. Sure, it takes a bit to get used to, but in the end your code becomes a tiny bit more verbose, and I feel in this case it's a bit better rather than making your code as short as possible, at the cost of being less readable.

Now let's check out what IsaacScript made of my TypeScript code– oh..

Oh no...

Well, surely, the code is there to truly emulate TypeScript and make it native, right?

Oh no...

It's.. including EVERY standard library JavaScript function. All loaded in the global table. It's greatly polluting it, with just unnecessary TypeScript functions that don't even ever get used in the final code.

That's not good, at ALL. But, sure, let's overlook that, and see what the actual generated code is – besides all the global namespace function clutter.

function postGameStarted(self)
    Isaac.DebugString("Callback triggered: MC_POST_GAME_STARTED")
    local p = __TS__ArrayFilter(
        __TS__ArrayMap(
            Isaac.FindByType(EntityType.ENTITY_PLAYER),
            function(____, p) return p:ToPlayer() end
        ),
        function(____, p) return p and (not p:IsCoopGhost()) end
    )
    __TS__ArrayForEach(
        p,
        function(____, player)
            player:AddKeys(10)
        end
    )
end

Oh. That's, okay, I guess?? It's still a LOT of clutter for something that can be done with a single for loop, creating new arrays and calling tons of function on them.. This can all be done so much better, and lower-level access in a modding API like this is incredibly important. (We'll explore this more later on.)

I can't imagine how much worse this gets later when you create code on a bigger scale, more than just giving every player 10 keys.

I'll put the entirety of the original code and transpiled code here, just if you want to read through it yourself.

But, in the end, it all works. So it can't get much worse than this, right? Just let people use IsaacScript if they really want to and feel like it?

The Creator

I had a few issues with IsaacScript, and I thought, why do all this jank when I can instead contact the contributors for help?

To put it bluntly - reaching out for help didn't, well, help at all. The very first message I was met with was:

i dont think anyone in the community codes mods from a linux computer, so things might be a little rocky for you
don't you have to use an emulator to even run isaac on linux?

Let's quickly take a moment to step aside and talk about IsaacScript's creator, Zamiel. They're a notorious figure, and who people associate IsaacScript with.

Their main strategy for getting IsaacScript popular seems to be to suggest it to people who are clueless; those who don't know much about Isaac modding, see IsaacScript, look at the features list, and be instantly convinced this is what every modder out there uses.

This could be farther from the truth. If you even jokingly say you're going to be using IsaacScript, most people in the modding community will give you a swift "why?".

This is why they seem to be insistent on pushing the idea that IsaacScript is superior to everything else. If you don't use IsaacScript, then you really should, else you'll waste loads of time is more or less what they live by. (I don't mean to be a prick by saying this, but from my experiences this is what it's usually like.)

Of course, they're quickly greeted with the obstacle that TypeScript is somewhat harder to learn than Lua (as Lua is specifically designed for beginners, though this is somewhat opinionated), and IsaacScript has severely less users than standard Lua modding. You can't just paste a Lua snippet of code into TS, you have to specifically convert it, and while there are tools to automatically do this, they're still not the best and still contribute as extra work. Most of the community uses Lua and creates Lua snippets, and not everyone's going to offer TypeScript versions or TypeScript support.

This creates a massive problem of marketing something that's very clearly meant for more advanced users who know what they're doing to beginners to Isaac modding and coding in general.

Though it is fair that there will be beginners to Isaac modding who know TypeScript and not Lua, as TypeScript is much bigger than Lua, it's important to not always use the only language you know and apply it everywhere, as it's not going to fit all use cases. Different languages are designed for different purposes, and Lua has the C API, allowing you to integrate Lua into, say, a modding API, as one of its main goals. (I, myself, am somewhat guilty of this. TypeScript is still the only language I know which can do a lot of what I wish to achieve with projects.)

The easy solution would be to not do that, but Zamiel seems insistent on this, despite the majority of users, even when offered to use IsaacScript, would prefer to just use plain Lua. Zamiel will always offer IsaacScript, often specifically omitting downsides to make it look better.

If you remember the aforementioned IsaacScript website, there's also a page called "Is IsaacScript Right for Me?". It's.. basically just saying yes for any project that actually matters, for any person:

How good are you at coding? Beginner - Beginners need someone to hold their hand and tell them when they're made a typo. The TypeScript compiler is exactly that - a helpful friend that looks out for you.
Note the very clearly marked "beginner" spot being just a "yes".

I do somewhat believe what they're saying - it's easier to set up IsaacScript than to bother with language servers, IntelliSense, all that garbage. But in the end, you get a setup which functionally is the same, except without a compiler, and I feel it's somewhat bad to encourage beginners to use your alternative, not being aware of the overhead or downsides.

While you could say to a beginner, setting up IsaacScript is faster than dealing with the Lua equivalent setup, why not create templates for beginners? In the end, IsaacScript is mainly just the TS2L transpiler with some other tools stacked on it (ESLint, Prettier, etc.) and a tool that sets everything up for you. Why shouldn't the Lua equivalent of this exist as well?

And, honestly? This post could just end here if it was that simple. But really, Zamiel's always like this. While some of my remarks are a tiny bit exaggerated, they still are true to an extent. In fact, I'm pretty sure they purposefully omit downsides to make it look more appealing.

In fact, if you check the Isaac modding FAQ that, none other than Zamiel has made, it lists IsaacScript as a resource to help you get started with modding.

Let's jump back to my conversation with them about the issues I'm having. Their first response is to tell me to make a pull request...

a lot of the checks you are talking about assume that you are on a windows computer. if you like, you can edit the source code to handle linux more gracefully, and submit a pull request

While, keep in mind, I still have no clue why exactly it's not following symlinks. I don't have the time to sit through IsaacScript's codebase, trying to figure out what exactly in the code stack is making it ignore symlinks, fix it, lint the code, build the code, test the code and submit a PR.

While, keep in mind, I'm still trying the project out. I can't get it to work at all without these issues fixed. It's not a good first impression to try a project out, and be greeted with "just submit a pull request".

What am I greeted with for expressing this?

If you want to use IsaacScript, you could simply not symlink your mods directly.

Ah, okay. If you don't want to have [issue], just don't do [thing that causes issue]! Simple! Thanks! I'll be sure to mark the thread as solved!

Afterwards? Oh, nothing happened afterwards. Someone else in the server kindly tried to help, but that didn't go anywhere, and a few days later after writing this article and showing it to a frequent contributor of IsaacScript a pull request got pushed which fixed what things I was having troubles with. (I will be forever grateful...)

I will admit, I didn't contact them in the nicest of ways. I expressed my distaste for IsaacScript right out the gate, and the first impression I got with absolutely nothing working frustrated me a bit. I apologized for it, but that didn't help fixing the issue whatsoever.

To leave off this section, I'll just.. give you a snippet of a conversation to give you a better impression of how Zamiel is like in the Isaac modding Discord.

Someone said that they're happy with their modding setup without TypeScript. Zamiel replies with "why not just use TypeScript".

Okay, well, let's say, I'm the only one with these issues. How well does it actually work? How do others say how well it works?

It Gets Worse (Testimonies)

So, I may not have gotten the best experience with IsaacScript because of my setup, but what I do have is testimonies of other people using IsaacScript. Oh, they're bad.

Let's start with the most obvious one. Lua doesn't compile to TypeScript with zero overhead. Zamiel likes to ignore the overhead when running said code and focus more on the build times, which, I will admit, TypeScript build times aren't bad. (I've certainly seen worse. 🦀)

From a skim of the outputted code [IsaacScript] seemed to have quadrupled[1] the line count in most of the functions I looked at compared to normal Lua (ignoring the branching functions where it tries to emulate some features)

[1] I've been told by the author of this quote that quadrupled may be inaccurate - "More accurately it doubled/tripled to the code size. [...] Your mileage may vary."

And, from looking at compiled code written in IsaacScript - it's correct! I can find way better ways to do most of what's done in the compiled code in regular Lua (as seen in the "My attempts at running IsaacScript" section) and it's important to have direct lower level access to this sort of stuff in a modding API of all things. While it's true that evaluating code quality by code length isn't good, it's still important to consider what those lines are taking up. And what those lines are taking up is unnecessary, heavily optimizable code, from what I've seen of it.

If you've ever played modded Minecraft (or any modded game for that matter), you'll know the feeling of playing on a modpack (or creating one) and having no clue why the game is laggy. It could be because of this mod, or it could be because of that one, and most often there aren't any intuitive tools for checking which mod is causing lag. You have to run through each mod, try to estimate how intensive each mod is, and disable it. At most points, if an end user gets to this point, they'll likely reconsider modding in the first place.

My point is, it's important for all mods to consider optimization. A typical user will go to the Steam Workshop, pick out a handful of mods, and then whatever happens with them they'll just take as all of modding. While it's bad to prematurely optimize everything, you have to admit there's definitely very obvious optimizations to be done here, just reading through the code. You could use IsaacScript to convert .filter to a translated Lua version of JS's filter, or you could manually handle for loops, which will make your code run smoother in the end.

And this isn't just something I'm making up just to make my point more clear – creating an array, running an anonymous function for each element of the array to create a new array, and then running another anonymous function for each element of the array again will always be slower than a for loop, something meant for this purpose, especially if you consider how your code runs.

I'm not saying that TypeScript mods will make your game lag much more, but it does pile up, and an average end user won't be able to identify why something lags (though this is somewhat possible with Mod Profiler, a mod I originally made, it's still janky and not something the end user will always be willing to go through). While you could argue that the code size in the main.lua doesn't matter, it's still code the game has to run. The more lines there are, naturally, the slower it'll get.

(This paragraph has been only added as of 16 Dec, 2021.) Another important thing worth to mention is how IsaacScript users, developers and contributors usually only consider themselves when making tools for modding - for example, the IsaacScript typings, which are completely in TypeScript are often better than the unofficial Isaac modding API docs, and in fact, Zamiel uses this as a bragging point on the website, despite contributing to the docs themselves??

Another noticeable thing from reading the code is that it's miles more unreadable. Both to me, and others who have seen transpiled code.

A typical reaction for someone posting transpiled IsaacScript code.
Perfectly readable code.

When a beginner decides to get into Isaac modding, suggesting them to read how other mods do X is a very good suggestion. As all mods are open source by nature, you can just read how something functions just by reading the lua files.

For example, Retribution is an excellent example of how to make a mod. It covers enemies, items, characters, and all of them are done really well.

If you, as a beginner, open a TypeScript mod, and it's all.. this, you're likely going to get discouraged from modding in general. While it is still possible to read the original .ts files in most cases, that's still a whole different language.

I learnt initial Isaac modding by looking at Universal Dice, and seeing how it does it - it's a mod that adds an active item is all that's important here. I looked through how callbacks work, I looked through how items.xml works, and I used all of this to create the initial version of Dream - where it was still a single character and a single item.

Imagine how different that would've been if the mod was written in TypeScript, and the main.lua was just transpiled code. I would've been heavily put off by the giant amount of code, and likely have moved on with my life, thinking the Isaac modding community uses a TypeScript transpiler for every mod. (While this is just my scenario, and a fictional one, with my previous opinions about TypeScript I hope you'll understand this is how it'd most likely go.)

It's true that it's possible that wouldn't happen, but I'd still have to try to find what transpiler it's using (as the linked transpiler in the comments is just the base TS2L one, not IsaacScript), set up another environment after already creating one under the assumption the modding community uses Lua, and it'd be a whole mess.

Another thing you might notice from this screenshot is the TypeScript transpiler includes everything. It includes all standard library JS functions even if you don't need them. Which, boy, that sure sounds like bloat to me!

As one last thing about IsaacScript, I want you to remember TypeScript was never meant to transpile to Lua. The modding API was never meant to work with TypeScript either, and while you may try, you'll probably need to use a different language to work around the issues that TypeScript causes. I hope that helps you decide which one to use even after so much rambling.


Summary

So, what did we learn today? While it's okay to use IsaacScript, it's generally not a very good idea. It leads to issues compiling the code (because, you have to remember, that's a thing you need to do!), it leads to the resulting code being unreadable, which throws off coding beginners looking to learn modding, it makes teamwork severely harder, and its branding as the solution to all Isaac modding problems, especially to beginners, is misleading.

And I just want to clarify, if you're just using IsaacScript, I don't hate you. I hate people like Zamiel who force it upon others, and pretend it isn't a tool for advanced users only. And since IsaacScript's branding is based around the idea that "IsaacScript is the definitive way to make Isaac mods", some of its decisions are often very odd.

You often see TypeScript users paste blocks of code in TypeScript, or prioritize TypeScript solutions over plain Lua ones, especially from Zamiel. This is because they want to standardize IsaacScript, to have it be the main option for modding and what everyone agrees upon is the real way to code Isaac mods, as this makes their lives much easier.

But I firmly believe that this will never happen because of the issues expressed above; IsaacScript both on paper and in practice is too far from a complete, seamless tool to be shoved willy-nilly in all projects and recommended to anyone that picks up Isaac modding. And before then, it's going to just annoy people who don't use IsaacScript, and that certainly won't help anyone switch to it.

Not everyone is going to be immediately willing to switch to IsaacScript, even as an advanced user, and it's completely fine to accept this. Some just don't want to deal with another huge chunk of new potential ways for the game to unexpectedly error due to issues out of their control, and some just don't want to bother with learning it or setting it up. No software has no bugs, and the more middleware you use between writing code and testing it out in-game, the higher the chance for error. And not everyone is willing to deal with TypeScript.

And after all, switching to a different language is going to be hard. You're going to be met with a lot of unfamiliarity, a lot of uncertainty and a lot of things you're not quite used to. You're going to be met with lots of new possible options for code failures, lots of new things to consider, and it's often overwhelming. You get less support, you have a smaller community, the list of things to consider is extremely large, yet Zamiel likes to brush it off as that makes more beginners flock to IsaacScript.

And is it really worth it for some feature that can already be done with standard Lua?

I'd recommend reading Zamiel's response to this article - it brings up a lot of good points I failed to mention.

Credits

Big thanks to Kyojo (and other members of the Forgotten Fables mod team), Aether, Somdudewillson and Keeper for proof-reading the article and pointing out the mistakes I made. (I made sure to invite people from both sides of the argument to proof-read this article and trust me, mistakes were plentiful.)

And I still want to thank Zamiel (and TS2L contributors) for their efforts trying to make IsaacScript as good as it is right now. In the end, sure, I dissed IsaacScript quite a bit, but I do genuinely believe that tons of work has been put into it, and I know others enjoy IS. Shoutouts to them and the rest of the IsaacScript contributors for putting this much work into the project.

Though, if you still believe I made factual errors in this article, be sure to contact me! I'll be more than glad to correct anything I might've gotten wrong. I don't want to misrepresent one side or the other, after all.


published