Nick Lemmon

A Typescript Monster Battler

February 12, 2019

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:

  1. Use Node.js to build a text-only interface,
  2. Make use of ES6 modules,
  3. 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

Nick Lemmon

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.