Blog » Making Grass, part 2: TypeScript
Making Grass, part 2: TypeScript
- Making roguelikes
- Making Grass
- Making Grass, part 2: TypeScript
- Making Grass, part 3: Scene graph and performance
- Making Grass, part 4: Pathfinding
- Making Grass, part 5: Field of Vision
TypeScript
At some point Kos suggested I try TypeScript. Initially I was happy hacking away at the project in plain JS, but as the code became bigger and started spilling into multiple files, I decided to try it out.
It turns out TypeScript is pretty easy to introduce, because legal JavaScript code just works. Then you can start introducing some types to your functions and variables, and the compiler will tell you if anything doesn't match. Unless you're doing something weird, most of your JavaScript should be easy to annotate.
Here is where it helped:
-
The obvious first place is enums. Instead of
'ATTACK'
and'MOVE'
I can doActionType.ATTACK
andActionType.MOVE
. Well, this I can do in plain JavaScript, but thanks to TypeScript a typo is a compilation error. I can also expect anActionType
and not a plain string. And more importantly, if I want to make sure myswitch
statement covers all the options, the compiler is able to verify that. -
Union types are fun. Suppose I wanted to have an object that describes an action: I can
ATTACK
a mob,MOVE
to a position, orOPEN_DOOR
at a given position. I could define a type with optionalmob
andpos
fields:interface Action { type: ActionType; mob?: Mob; pos?: Position; }
However, this is not 100% safe. I could access
action.pos
for an action that doesn't have a position attached. Instead, I can define a union of interfaces, withtype
field acting as a tag:interface MobAction { type: ActionType.ATTACK; mob: Mob; } interface PosAction { type: ActionType.MOVE | ActionType.OPEN_DOOR; pos: Pos; } type Action = MobAction | PosAction;
The compiler will recognize that I'm checking the type of an action:
// Invalid: action.pos can be undefined console.log(action.pos.x, action.pos.y); if (action.type === ActionType.ATTACK) { // Valid: we know that action is of type MobAction here console.log(action.pos.x, action.pos.y); }
It's slightly more verbose than in languages like Haskell (where I would just write
Attack(Mob) | Move(Pos)
, but still pretty convenient. And while the OOP solution to the problem would be to give the control to theAction
class (and perhaps use a Visitor pattern), so far treating actions as data is working out well. It's nice to have that option. -
Finally, once you have everything nicely set up, strict mode and strict null checks are where TypeScript really shines. Suddenly, variables of a given type (say,
Pos
) cannot be null or undefined! You have to usePos | null
andPos | undefined
for that. TypeScript will not allow you to pass a null value as aPos
, and will require you to check for null (for instance, by addingif (pos)
).If you really want to, escaping the system is simple: there is an assertion operator (exclamation mark:
x!
) that tells the compiler you know what you are doing (although without adding a run-time check).This is similar to
Optional<X>
types in various languages, but again, without any additional syntax. What's nice is that it already makes the type system stronger than in traditional languages like Java, where even if you are usingOptional
, nothing is stopping you from passing a null anyway; and moves it closer to more water-tight systems like Haskell or Rust.I know that nowadays C# allows you to enable a similar option (Nullable Reference Types). I haven't tried it out but it looks like a similar idea.
It's sometimes noticeable that TypeScript is a bolted-on type system, and not a language in itself. There are ways to shoot yourself in the foot. Also, it's still JavaScript, so some things are awkward and you have to make compromises. But TypeScript gets you to that place of "if your code compiles with no errors/warnings, it probably works" usually associated with statically typed languages.
Also, I have to say that writing TypeScript with Visual Studio Code is really pleasant. The editor is relatively lightweight (if you can say that about an Electron-based application), but you get instant error checking, accurate auto-completion (thanks to the typing annotations) and automatically added imports.
Parcel
Since I'm using TypeScript now, I have a compilation step and can't just serve the files directly anymore. I have some experience with Webpack for packaging my JavaScript, but it's not the simplest to set up. Even a small project (like my Minefield) involves ~60 lines of configuration, and installing several plugin packages.
So I decided to try out Parcel which advertises itself as zero-configuration. The start is really smooth: just say parcel index.html
and it will examine your file, bundle all the JavaScript and CSS, and set up a dev server. TypeScript will also magically work. Other asset types are also supported out of the box (well, you need to have the appropriate compiler installed). And it's pretty fast, although the auto-reloading tends to break randomly.
The dark side is that Parcel is not very flexible. It doesn't have a configuration file on principle (probably as a reaction to community's painful experiences with Webpack), with developers basically treating it as a slippery slope. You can of course customize TypeScript, or other tools used by Parcel, but once you want to have to customize Parcel itself, you're on your own.
So far I cut myself once: I wanted Parcel to stop trying to "package" links like ?map=2
(instead of index.html?map=2
) and complain they will not work. Since there is no configuration file, as far as I know this would require (1) not using the Parcel CLI script and writing my own entry point, or (2) writing a separate Node package with a plugin for Parcel. I decided I didn't want it that badly and instead added a dirty hack that modifies the DOM.
- Making roguelikes
- Making Grass
- Making Grass, part 2: TypeScript
- Making Grass, part 3: Scene graph and performance
- Making Grass, part 4: Pathfinding
- Making Grass, part 5: Field of Vision