A Typescript Monster Battler
I was inspired to finally bite the bullet and try out Typescript after listening to NPM's Laurie Voss on Shop Talk Show Episode 345. The following is a brief run through of what I discovered along the way.
A Project
I've always been curious about game development, and as such, wanted to use this opportunity to dip my toe in to some basic text-based gaming mechanics through a Node.js application. I knew I could pretty quickly build some JavaScript modules for rolling dice, determining NPC stats, and ultimately, simple turn-based combat. Why not spice things up with Typescript while I'm at it?
Tools
I had some basic architectural requirements I wanted to stick with when authoring my first TypeScript app:
- Use Node.js to build a text-only interface,
- Make use of ES6 modules,
- and use Jest for unit testing
This structure was mainly meant to mimic common tools I typically use during day-to-day work.
Getting Started
The Build
ES6 Modules in Node.js
I've gotten used to writing JavaScript modules using the newer,
ES6 import
syntax. Node.js (at least at the time of writing!) does not play nicely with
said syntax by default, but that's easy to resolve with a build in flag and the
.mjs
file extension:
node --experimental-modules ./index.mjs
TypeScript
After initializing a project through NPM, installing TypeScript is easy enough:
npm install typescript --save-dev
I then leveraged NPM's somewhat new npx
feature to run the TypeScript CLI and
compile .ts
files to JavaScript.
Handling .mjs
But wait a minute, don't these files need to be compiled to .mjs
rather than
.js
if I want to use ES6 imports? A small file named toMJS.bash
to the
rescue:
#!/bin/bash
for file in ./dist/*.js
do
mv "$file" "${file%.js}.mjs"
done
Bringing the Build Together
Using NPM scripts, all of the commands can be brought together quite nicely via
the package.json
:
"scripts": {
"clean": "rm -rf ./dist",
"build": "npm run clean && npx tsc",
"prepare": "sh toMJS.bash",
"start": "npm run build && npm run prepare && node --experimental-modules ./dist/index.mjs"
}
Testing
Writing tests using Jest is a breeze - it's easy to get started, simple to configure, and runs tests efficiently. Making it work with TypeScript, however, did require a bit of know-how.
Playing Nicely with TypeScript
Installing Jest is very straightforward:
npm install jest --save-dev
Unfortunately, at least at the time of writing, Jest does not support TypeScript
out of the box. That's nothing a good npm install
can't solve!
npm install ts-jest @types/jest --save-dev
A little configuration in the jest.config.js
and we are up and running:
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
Playing Nicely with ES6 Imports
Like Node, Jest does not support ES6 modules out of the box. Once again, our
friend npm install
will save us.
npm i @babel/core @babel/preset-env babel-jest --save-dev
And just some tiny configuration in a .babelrc
file:
{
"presets": ["@babel/preset-env"]
}
Writing Some Modules
I won't go in to detail regarding all of the modules in the project, as those can easily be found in the GitHub Repo, however, I hope to go over how I leveraged TypeScript features when authoring.
Rolling a Die
Rolling "dice" is pretty straightforward. A simple JavaScript function can
handle the work, and with TypeScript, required parameters have types. Here is
rollDie.ts
:
export default function rollDie(numberOfSides: number = 6) {
return Math.floor(Math.random() * numberOfSides) + 1
}
Types paired with default parameters really offer some power! Not only will future developers be restricted to passing in numbers when it comes to the number of sides on a virtual die, but they won't have to pass in anything if they want to use a traditional 6-sided die.
A Monstrous Interface
Interfaces allow consistent, predictable structuring of objects, and in this project's case, monsters!
export default interface Monster {
name: string
strength: number
dexterity: number
toughness: number
vitality: number
}
And later, in our index.ts
, we create a Cyclops
that leverages the Monster
interface:
const Cyclops: Monster = {
name: 'Cyclops',
strength: 10,
dexterity: 2,
toughness: 8,
vitality: 10,
}
We can even use this interface to build other functions, particularly functions
that accept a Monster
as an argument. For example, the getHP()
function:
import Monster from './Monster'
export default function getHitPoints(monster: Monster) {
return monster.toughness + monster.vitality * 2
}
Final Thoughts
Authoring with TypeScript has some clear benefits. Predictability and consistency are enforced via a system of checks that occur when TypeScript is compiled, reducing the risk of defects and making code more concise.
Though the benefits are clear, ideally, both Node and Jest would support TypeScript out of the box. Getting a working development environment is feasible with current tools, however, takes a little more work than I expected considering the popularity of the language. Hopefully as the web development community continues to welcome our new TypeScript overlords, the tooling can catch up appropriately.
Helpful Links
- The full TypeScript Monster Battler repo
- ts-jest NPM package to leverage TypeScript with Jest
About Me
I’m a frontend engineering manager working for Truist Financial currently based in in Columbia, Maryland.
I’m driven to spearhead, design, and build accessible design systems and web applications with a great underlying developer experience in mind.