Using TypeScript in Phaser

I’m about to start on my second game project in Phaser. Cross or Croak helped me get familiar with the library, but I want to step up my game for my next project. I think TypeScript is a great place to start! I found a sample project on the Phaser site and combed through the TypeScript documentation to learn what I need to get going. Keep reading and I’ll catch you up!

Why TypeScript?

If there’s one thing I learned building Cross or Croak, it’s that even a simple game project is more complicated than you expect. I am a web developer professionally, and I would say building this game was more challenging than anything I’ve seen on the job.

Part of this challenge was the JavaScript language. I love JavaScript. It’s powerful, it's flexible, but it lets you be sloppy. It’s easy to write messy code that’s hard to fix. For large projects, like games, it can be helpful to set rules and boundaries. You don’t want to code yourself into a corner. Adding strong typing with TypeScript is an easy way to avoid this kind of situation. Let's get it installed and talk about how we can use it in our projects.

Setup and Phaser definitions

You can add TypeScript to your project using NPM or Yarn:

Terminal

npm install -g typescript

yarn add typescript

TypeScript is compiled into JavaScript from .ts files. You can configure the way they are compiled with a tsconfig.json file.

tsconfig.json

{
    "compilerOptions": {
      "target": "ES2016",
      "module": "CommonJS",
      "sourceMap": true,
      "noImplicitAny": true,
      "strict": true,
      "strictPropertyInitialization": false,
    },
    "include": [
      "src/**/*",
      "defs/*"
    ],
}

Notice the "defs/*" element we included in the "include" array. This is where we can put our Phaser TypeScript definitions, which let your IDE or text editor auto-suggest things from the Phaser library. You can find these definitions in the Phaser project on GitHub.

Now that we've added TypeScript to our project, let's get into what we can add to our code!

Class Properties

The first place you’ll start using TypeScript is when you define classes: your Scenes, Sprites, Maps, etc. Before the constructor, we define the class properties at the top of the class. This puts the data used by the class in a convenient location, and defines what that data can look like and how it can be used.

GameScene.js

class GameScene extends Phaser.Scene {
  height: number;
  width: number;
  ...
}

Type Annotations

When we define a class property, we create a rule that says the property has to be a certain type. That rule is called a contract. The syntax of this contract looks like this: height: number. If we try to set height to something other than a number, we get an error when we compile the project. This catches errors early, before they have a chance to break your game.

Return Types and Parameters

Class properties aren't the only place we can use type annotations. We can define the types of a method's parameters, as well as the type of the value the method returns. The syntax looks like this:

Map.ts

getTile(x: number, y: number): Tile {
  return this.tiles[x][y];
}

Basic Types

You might be wondering what data types are available in TypeScript. You can create your own types, but the basic types built into TypeScript are:

  • boolean: holds true or false values
  • number: numbers go here - hexadecimal, binary, and octal literals are also accepted
  • string: hold a string, using the same quote or backtick syntax as JavaScript
  • array: define an array of elements of a type with number[] or Array
  • tuple: an array of fixed length, normally a pair, where the type of each element is known [string, number]

Other Types

Beyond these simple types, there are other types we use in specific places. These types and where we use them are outlined below.

  • any: can hold any type, useful when we’re unsure about what we’re getting
  • void: no type, used for the return type of functions that return no value
  • null & undefined: usually used in union types when —strictNullChecks is on
  • never: used as the return type when a function does not return at all

Data Modifiers

We can also apply data modifiers to our class properties. These modifiers are public, private, static, and readonly. These define how the property can be used.

  • public: can be accessed anywhere without restrictions
  • private: are only visible to the class, not accessible outside the class
  • readonly: can be accessed outside the class, but not modified outside the class
  • static: can be accessed from the class without instantiating an object

Classes, Interfaces, and Enums as Types

We can use classes, interfaces, and enums defined in the same file or imported from other files as types for our class properties. We can also use classes from the Phaser library as types.

Interfaces define a set of properties that the typed objects must have. They can have more properties, but they must have all of the properties defined in the interface.

An enum is a set of strings mapped to numbers, useful when you have a group of named options.

GameScene.ts

import Phaser from "phaser";
import Player from "./Player";

enum Color {Red, Green, Blue}

class GameScene extends Phaser.Scene {
  height: number;
  width: number;
  tilemap: Phaser.Tilemaps.Tilemap;
  player: Player;
  color: Color;
}

TypeScript in Practice

I haven’t had a chance to use TypeScript in a game project yet, so let’s take a look at some ways @mipearson used it in his game Dungeon Dash.

This game uses the Dungeon Factory (now called Dungeoneer) library to generate random dungeon layouts. The developer uses TypeScript to create a contract for the output of this library and the maps he builds from it.

Map.ts

interface DungeonFactoryOutput {
  tiles: Array<Array<{ type: string }>>;
  rooms: Array<{ height: number; width: number; x: number; y: number }>;
}

export default class Map {
  public readonly tiles: Array<Array<Tile>>;
  public readonly width: number;
  public readonly height: number;
  public readonly tilemap: Phaser.Tilemaps.Tilemap;
  public readonly wallLayer: Phaser.Tilemaps.StaticTilemapLayer;

  public readonly startingX: number;
  public readonly startingY: number;

  constructor(width: number, height: number, scene: Phaser.Scene) {
    const dungeon = DungeonFactory.generate({
      width: width,
      height: height
    }) as DungeonFactoryOutput;
    ...
  }
  ...
}

Here you can see examples of nearly every concept we talked about. Class properties are given type annotations and data modifiers to define what they hold and how they are used. Custom classes from the project and the Phaser library are define the types of some class properties. An interface is used to create a contract for the output of the DungeonFactory library, using the as keyword. This all works together to create a tight, easy-to-use class for generating random tilemaps. Neat!

I'm hoping these TypeScript features help to manage my next Phaser project. I have some fun ideas for new games, and I want to put them together in a reasonable amount of time with few roadblocks. I hope this article helps you avoid roadblocks in your projects, too! Thanks for reading!