Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Mario5 with TypeScript

5.00/5 (28 votes)
18 Dec 2014CPOL27 min read 79.9K   2.1K  
Exploring the abilities, benefits and features of TypeScript by extending / rewriting the original Mario5 source.

Mario5 TypeScript

Contents

  1. Introduction
  2. Background
  3. Converting the existing project
    1. References
    2. Annotations
    3. Enumerations
    4. Interfaces
    5. Classes
    6. Fat arrow functions
  4. Extending the project
    1. Default parameters
    2. Overloads
    3. Generics
    4. Modules
  5. Using the code
  6. Points of Interest
  7. History

Introduction

One of my most epic moments at CodeProject was the release of the article about Mario5. In the article I describe the making of a game based on web technologies such as HTML5, CSS3 and JavaScript. The article gained a lot of attentation and is probably among the ones where I am really proud of.

The original article uses something I described as "OOP JavaScript". I wrote a small helper script called oop.js, which allowed me to use a simple inheritence / class pattern. Of course JavaScript is very object-oriented from the beginning. Classes are no direct criteria for OOP. Nevertheless, the pattern helped a lot to make the code both, easy to read and maintain. This is obtained by not having to deal with the prototype scheme directly.

With TypeScript we get a unified class construct in JavaScript. The syntax is based on the ES6 version, preparing TypeScript to remain a full superset of JavaScript even with ES6 implementations. Of course TypeScript compiles down to ES3 or ES5, which means that this class construct will be decomposed to something that is available right now: again the prototype mechanism. Nevertheless, what remains is code that is readable, cross-implementation (ES3 / ES5) safe and agrees on a common base. With my own approach (oop.js) no one besides me did know what was going on without reading the helper code. With TypeScript a broad set of developers uses the same pattern, as it is embedded in the language.

It was therefore just natural to convert the Mario5 project to TypeScript. What makes this worth an article on CodeProject? I think it is a nice study how to convert a project. It also illustrates the main points of TypeScript. And finally it gives a nice introduction to the syntax and the behavior. After all, TypeScript is easy for those who already know JavaScript and makes it easier to approach JavaScript, for those who do not have any experience yet.

Background

More than a year ago Anders Heijlsberg announced Microsoft's new language called TypeScript. This was a surprise for most people, as Microsoft (and especially Anders) seemed to be against dynamic languages, in particular JavaScript. However, it turned out, that Microsoft realized what a big opportunity the centralization of general purpose programming to web programming is. With JavaScript for Windows Store apps, the ongoing hype with node.js and the NoSQL movement with document stores that use JavaScript for running queries, it is obvious that JavaScript is definitely important.

Remark: Anders joined the TypeScript team at some point (v0.8) in the development. It is unclear if he invented the language or if somebody else came up with the idea. Nevertheless the team is currently lead by Anders and given his experience and expertise, it is certainly good to have him in charge of the project.

The realization did influence the decision on the design of a new language. Instead of creating a new language from the ground up (like Google did with Dart), Anders decided that any language that may be still established has to extend JavaScript. No solution should be orthogonal. The problem with CoffeeScript is that it hides JavaScript. This may be appealing for some developers, but for most developers it is an absolute exclusion criterion. Anders decided that the language has to be strongly typed, even though only an intelligent compiler (or transpiler to be more correct) will see these annotations.

So what happened? A true superset of ECMAScript 5 has been created. This superset has been called TypeScript to indicate the close relationsship with JavaScript (or ECMAScript in general), with the additional type annotations. Every other feature, such as interfaces, enums, generics, casts, ... follows from these type annotations. In the future TypeScript will evolve. There are two areas:

  1. Embracing ES6 to remain a true superset of JavaScript
  2. Bringing in further features to make JS development easier

The primary benefit of using TypeScript is two-fold. On the one side we can take the advantage of being informed of potential errors and problems during compile-time. If an argument does not fulfill a given signature, then the compiler will throw an error. This is especially useful when working with larger teams or an a bigger project. The other side is also interesting. Microsoft is known for their excellent tooling with Visual Studio. Giving JavaScript code a good tooling support is tedious, due to the dynamic nature of JavaScript code. Therefore even simple refactoring tasks such as renaming a variable cannot be performed with the desired stability.

In the end TypeScript gives us great tooling support combined with a much better idea about how our code will work. The combination of productivity plus robustness is the most appealing argument for using TypeScript. In this article we will explore how to convert existing projects. We will see, that transforming a code to TypeScript can be done incrementally.

Converting the existing project

TypeScript does not hide JavaScript. It starts with plain JavaScript.

JavaScript logo

The first step in utilizing TypeScript is of course to have TypeScript source files. Since we want to use TypeScript in an existing project, we'll have to convert these files. There is nothing to do here as a requirement, however, we'll just rename our files from *.js to *.ts. This is just a matter of convention, nothing that is actually required. Nevertheless, as the TypeScript compiler tsc usually considers *.ts files as input, writing *.js files as output, renaming the extension ensures that nothing wrong happens.

The next subsections deal with incremental improvements in the conversion process. We now assume that every file has the usual TypeScript extension *.ts, even though no additional TypeScript feature is used.

References

The first step is to supply references from single JavaScript files, to all other (required) JavaScript files. Usually we would only write single files, which, however, (usually) have to be inserted in a certain order in our HTML code. The JavaScript files do not know the HTML file, nor do they know the order of these files (not to speak of which files).

Now that we want to give our intelligent compiler (TypeScript) some hints, we need to specify what other objects might be available. Therefore we need to place a reference hint in the beginning of code files. The reference hint will declare all other files, that will be used from the current file.

For instance we might include jQuery (used by, e.g., the main.ts file) by its definition as via:

JavaScript
/// <reference path="def/jquery.d.ts"/>

We could also include a TypeScript version of the library, or the JavaScript version, however, there are reasons for including the definition file only. Definition files do not carry any logic. This will make the file substentially smaller and faster to parse. Also, such files will usually contain much more / better documentation comments. Finally, while we would prefer our own *.ts files to *.d.ts files, in case of jQuery and other libraries the original has been written in JavaScript. It is unclear, if the TypeScript compiler is satisfied with the source code. By taking a definition file, we can be sure that everything works.

There are reasons to write plain definition files outselves, as well. The most basic one is covered by the def/interfaces.d.ts file. We do not have any code, which would make a compilation irrelevant. Referencing this file on the other hand makes sense, since the additional type information provided by the file helps in annotating our code.

Annotations

The most important TypeScript feature is type annotations. Actually the name of the language indicates the high importance of this feature.

Most type annotations are actually not required. If a variable is immediately assigned (i.e. we define a variable, instead of just declaring it), then the compiler can infer the type of the variable.

JavaScript
var basepath = 'Content/';

Obviously the type of this variable is a string. This is also what TypeScript infers. Nevertheless, we could also name the type explicitly.

JavaScript
var basepath: string = 'Content/';

Usually we do not want to be explicit with such annotations. It introduces more clutter and less flexibility than we aim for. However, sometimes such annotations are required. Of course the most obvious case appears, when we only declare a variable:

JavaScript
var frameCount: number;

There are other scenarios, too. Consider the creation of a single object, that may be extended with more properties. Writing the usual JavaScript code is definitely not enough information for the compiler:

JavaScript
var settings = { };

What properties are available? What is the type of the properties? Maybe we don't know, and we want to use it as a dictionary. In this case we should specify the arbitrary usage of the object:

JavaScript
var settings: any = { };

But there is also another case. We already know what properties might be available, and we only need to set or get some of these optional properties. In that case we can also specify the exact type:

JavaScript
var settings: Settings = { };

The most important case has been omitted so far. While variables (local or global) can be inferred in most cases, function parameters can never be inferred. In fact function parameters may be inferred for a single usage (such as the types of generic parameters), but not within the function itself. Therefore we need to tell the compiler what type of parameters we have.

JavaScript
setPosition(x: number, y: number) {
	this.x = x;
	this.y = y;
}

Transforming JavaScript incrementally with type annotations therefore is a process, that starts by changing the signature of functions. So what about the basics of such annotations? We already learned that number, string and any are built-in types, that represent elementary types. Additionally we have boolean and void. The latter is only useful for return types of functions. It indicates that nothing useful is returned (as JS functions will always return something, at least undefined).

What about arrays? A standard array is of type any[]. If we want to indicate that only numbers can be used with that array, we could annotate it as number[]. Multi-dimensional arrays are possible as well. A matrix might be annotated as number[][]. Due to the nature of JavaScript we only have jagged arrays for multi-dimensions.

Enumerations

Now that we started annotating our functions and variables, we will eventually require custom types. Of course we already have some types here and there, however, these types may be less annotated than we want to, or defined in a too special way.

Sometimes there are better alternatives offered by TypeScript. Collections of numeric constants, for instance, can be defined as an enumeration. In the old code we had objects such as:

JavaScript
var directions = {
    none: 0,
    left: 1,
    up: 2,
    right: 3,
    down: 4
};

It is not obvious that the contained elements are supposed to be constants. They could be easily changed. So what about a compiler that might give us an error if we really want to do nasty things with such an object? This is where enum types come in handy. Right now they are restricted to numbers, however, for most constant collections this is sufficient. Most importantly, they are transported as types, which means that we can use them in our type annotations.

The name has been changed to uppercase, which indicates that Direction is indeed a type. Since we do not want to use it like an enumeration flag, we use the singular version (following the .NET convention, which makes sense in this scenario).

JavaScript
enum Direction {
	none  = 0,
	left  = 1,
	up    = 2,
	right = 3,
	down  = 4,
};

Now we can use it in the code such as:

JavaScript
setDirection(dir: Direction) {
	this.direction = dir;
}

Please note that the dir parameter is annotated to be restricted to arguments of type Direction. This excludes arbitrary numbers and must use values of the Direction enumeration. What if we have a user input that happens to be a number? In such a scenario we can also get wild and use a TypeScript cast:

JavaScript
var userInput: number;
// ...
setDirection(<Direction>userInput);

Casts in TypeScript work only if they could work. Since every Direction is a number, a number could be a valid Direction. Sometimes a cast is known to fail a priori. If the userInput would be a plain string, TypeScript would complain and return an error on the cast.

Interfaces

Interfaces define types without specifying an implementation. They will vanish completely in the resulting JavaScript, like all of our type annotations. Basically they are quite similar to interfaces in C#, however, there are some notable differences.

Let's have a look at a sample interface:

JavaScript
interface LevelFormat {
    width: number;
    height: number;
    id: number;
    background: number;
    data: string[][];
}

This defines the format of a level definition. We see that such a definition must consist of numbers such as width, height, background and an id. Also a two-dimensional string-array defines the various tiles that should be used in the level.

We already mentioned that TypeScript interfaces are different to C# interfaces. One of the reasons is that TypeScript interfaces allow merging. If an interface with the given name already exists, it won't be overwritten. There is also no compiler warning or error. Instead the existing interface will be extended with the properties defined in the new one.

The following interface merges the existing Math interface (from the TypeScript base definitions) with the provided one. We gain one additional method:

JavaScript
interface Math {
	sign(x: number): number;
}

Methods are specified by specifying parameters in round brackets. The usual type annotation is then the return type of the method. With the provided interface (extension) the TypeScript compiler allows us to write the following method:

JavaScript
Math.sign = function(x: number) {
	if (x > 0)
		return 1;
	else if (x < 0)
		return -1;
		
	return 0;
};

Another interesting option in TypeScript interfaces is the hybrid declaration. In JavaScript an object is not limited to be a pure key-value carrier. An object could also be invoked as a function. A great example for such a behavior is jQuery. There are many possible ways to call the jQuery object, each resulting in a new jQuery selection being returned. Alternatively the jQuery object also carries properties that represent nice little helpers and more useful stuff.

In case of jQuery one of the interfaces looks like:

JavaScript
interface JQueryStatic {
    (): JQuery;
    (html: string, ownerDocument?: Document): JQuery;
    ajax(settings: JQueryAjaxSettings): JQueryXHR;
    /* ... */
}

Here we have to possible calls (among many) and a property that is directly available. Hybrid interfaces therefore require that the implementing object is in fact a function, that is extended with further properties.

We can also create interfaces based on other interfaces (or classes, which will be used as interfaces in this context).

Let's consider the following case. To distinguish points we use the Point interface. Here we only declare two coordinates, x and y. If we want to define a picture in the code, we need two values. A location (offset), where it should be placed, and the string that represents the source of the image.

Therefore we define the interface to represent this functionality to be derived / specialized of the Point interface. We use the extends keyword to trigger this behavior in TypeScript.

JavaScript
interface Point {
	x: number;
	y: number;
}

interface Picture extends Point {
	path: string;
}

We can use as many interfaces as we want, but we need to separate them with commas.

Classes

At this stage we already typed most of our code, but an important concept has not been translated to TypeScript. The original codebase makes use of a special concept that brings class-like objects (incl. inheritance) to JavaScript. Originally this looked like the following sample:

JavaScript
var Gauge = Base.extend({
    init: function(id, startImgX, startImgY, fps, frames, rewind) {
        this._super(0, 0);
        this.view = $('#' + id);
        this.setSize(this.view.width(), this.view.height());
        this.setImage(this.view.css('background-image'), startImgX, startImgY);
        this.setupFrames(fps, frames, rewind);
    },
});

Unfortunately there are a lot of problems with the shown approach. The biggest problem is that it is non-normative, i.e. it is no standard way. Therefore developers who aren't familiar with this style of implementing class-like objects, cannot read or write code as they usually would. Also the exact implementation is unknown. All in all any developer has to look at the original definition of the Class object and its usage.

With TypeScript a unified way of creating class-like objects exists. Additionally it is implemented in the same manner as in ECMAScript 6. Therefore we get a portability, readability and extensibility, that is easy to use and standardized. Coming back from our original example we can transform it to become:

JavaScript
class Gauge extends Base {
    constructor(id: string, startImgX: number, startImgY: number, fps: number, frames: number, rewind: boolean) {
        super(0, 0);
        this.view = $('#' + id);
        this.setSize(this.view.width(), this.view.height());
        this.setImage(this.view.css('background-image'), startImgX, startImgY);
        this.setupFrames(fps, frames, rewind);
    }
};

This looks quite similar and behaves nearly identical. Nevertheless, changing the former definition with the TypeScript variant needs to be done in a single iteration. Why? If we change the base class (just called Base), we need to change all derived classes (TypeScript needs classes to inherit from other TypeScript classes).

On the other hand if we change one of the derived classes we cannot use the base class any more. That being said, only classes, that are completely decoupled from the class hierachy, can be transformed within a single iteration. Otherwise we need to transform the whole class hierachy.

The extends keyword has a different meaning than for interfaces. Interfaces extend other definitions (interfaces or the interface part of a class) by the specified set of definitions. A class extends another class by setting its prototype to the given one. Additionally some other neat features are placed on top of this, like the ability to access the parent's functionality via super.

The most important class is the root of the class hierachy, called Base. It contains quite some features, most notably

JavaScript
class Base implements Point, Size {
	frameCount: number;
	x: number;
	y: number;
	image: Picture;
	width: number;
	height: number;
	currentFrame: number;
	frameID: string;
	rewindFrames: boolean;
	frameTick: number;
	frames: number;
	view: JQuery;

	constructor(x: number, y: number) {
		this.setPosition(x || 0, y || 0);
		this.clearFrames();
		this.frameCount = 0;
	}
	setPosition(x: number, y: number) {
		this.x = x;
		this.y = y;
	}
	getPosition(): Point {
		return { x : this.x, y : this.y };
	}
	setImage(img: string, x: number, y: number) {
		this.image = {
			path : img,
			x : x,
			y : y
		};
	}
	setSize(width, height) {
		this.width = width;
		this.height = height;
	}
	getSize(): Size {
		return { width: this.width, height: this.height };
	}
	setupFrames(fps: number, frames: number, rewind: boolean, id?: string) {
		if (id) {
			if (this.frameID === id)
				return true;
			
			this.frameID = id;
		}
		
		this.currentFrame = 0;
		this.frameTick = frames ? (1000 / fps / setup.interval) : 0;
		this.frames = frames;
		this.rewindFrames = rewind;
		return false;
	}
	clearFrames() {
		this.frameID = undefined;
		this.frames = 0;
		this.currentFrame = 0;
		this.frameTick = 0;
	}
	playFrame() {
		if (this.frameTick && this.view) {
			this.frameCount++;
			
			if (this.frameCount >= this.frameTick) {			
				this.frameCount = 0;
				
				if (this.currentFrame === this.frames)
					this.currentFrame = 0;
					
				var $el = this.view;
				$el.css('background-position', '-' + (this.image.x + this.width * ((this.rewindFrames ? this.frames - 1 : 0) - this.currentFrame)) + 'px -' + this.image.y + 'px');
				this.currentFrame++;
			}
		}
	}
};

The implements keyword is similar to implementing interfaces (explicitely) in C#. We basically enable a contract, that we provide the abilities defined in the given interfaces within our class. While we can only extend from a single class, we can implement as many interfaces as we want. In the previous example we choose not to inherit from any class, but to implement two interfaces.

Then we define what kind of fields are available on objects of the given type. The order does not matter, but defining them initially (and most importantly: in a single place) makes sense. The constructor function is a special function that has the same meaning as the custom init method before. We use it as the class's constructor. The base class's constructor can be called any time via super().

TypeScript also provides modifiers. They are not included in the ECMAScript 6 standard. Therefore I also do not like to use them. Nevertheless, we could make fields private (but remember: only from the view of the compiler, not in the JavaScript code itself) and therefore restrict access to such variables.

A nice usage of these modifiers is possible in combination with the constructor function itself:

JavaScript
class Base implements Point, Size {
	frameCount: number;
	// no x and y
	image: Picture;
	width: number;
	height: number;
	currentFrame: number;
	frameID: string;
	rewindFrames: boolean;
	frameTick: number;
	frames: number;
	view: JQuery;

	constructor(public x: number, public y: number) {
		this.clearFrames();
		this.frameCount = 0;
	}
	/* ... */
}

By specifying that the arguments are public, we can omit the definition (and initialization) of x and y in the class. TypeScript will handle this automatically.

Fat arrow functions

Can anyone remember how to create anonymous functions in C# prior to lambda expressions? Most (C#) devs cannot. And the reason is simple: Lambda expressions bring expressiveness and readability. In JavaScript everything is evolving around the concept of anonymous functions. Personally, I only use function expressions (anonymous functions) instead of function statements (named functions). It is much more obvious what is happening, more flexible and brings a consistent look and feel to the code. I would say it is coherent.

Nevertheless, there are little snippets, where it sucks writing something like:

JavaScript
var me = this;
me.loop = setInterval(function() {
	me.tick();
}, setup.interval);

Why this waste? Four lines for nothing. The first line is required, since the interval callback is invoked on behalf of the window. Therefore we need to cache the original this, in order to access / find the object. This closure is effective. Now that we stored the this in me, we can already profit from the shorter typing (at least something). Finally we need to hand that single function over in another function. Madness? Let's use the fat-arrow function!

JavaScript
this.loop = setInterval(() => this.tick(), setup.interval);

Ah well, now it is just a neat one-liner. One line we "lost" by preserving the this within fat-arrow functions (let's call them lambda expressions). Two more lines have been dedicated to preserving style for functions, which is now redudant as we use a lambda expression. In my opinion this is not only readable, but also understandable.

Under the hood, of course, TypeScript is using the same thing as we did before. But we do not care. We also do not care about MSIL generated by a C# compiler, or assembler code generated by any C compiler. We only care about the (original) source code being much more readable and flexible. If we are unsure about the this we should use the fat arrow operator.

Extending the project

TypeScript compiles to (human-readable) JavaScript. It ends with ECMAScript 3 or 5 depending on the target.

TypeScript logo

Now that we basically typed our whole solution we might even go further and use some TypeScript features to make the code nicer, easier to extend and use. We will see that TypeScript offers some interesting concepts, that allow us to fully decouple our application and make it accessible, not only in the browser, but also on other platforms such as node.js (and therefore the terminal).

Default values and optional parameters

At this stage we are already quite good, but why leave it at that? Let's place default values for some parameters to make them optional.

For instance the following TypeScript snippet will be transformed...

JavaScript
var f = function(a: number = 0) {
}
f();

... to this:

JavaScript
var f = function (a) {
    if (a === void 0) { 
    	a = 0; 
    }
};
f();

The void 0 is basically a safe variant of undefined. That way these default values are always dynamically bound, instead of default values in C#, which are statically bound. This is a great reduction in code, as we can now omit essentially all default value checks and let TypeScript do the work.

As an example consider the following code snippet:

JavaScript
constructor(x: number, y: number) {
	this.setPosition(x || 0, y || 0);
	// ...
}

Why should we ensure that the x and y values are set? We can directly place this constraint on the constructor function. Let's see how the updated code looks like:

JavaScript
constructor(x: number = 0, y: number = 0) {
	this.setPosition(x, y);
	// ...
}

There are other examples, as well. The following already shows the function after being altered:

JavaScript
setImage(img: string, x: number = 0, y: number = 0) {
	this.view.css({
		backgroundImage : img ? c2u(img) : 'none',
		backgroundPosition : '-' + x + 'px -' + y + 'px',
	});
	super.setImage(img, x, y);
}

Again, this makes the code much easier to read. Otherwise the backgroundPosition property would be assigned with default value consideration, which looks quite ugly.

Having default values is certainly nice, but we might also have a scenario, where we can safely omit the argument without having to specify a default value. In that case we have still to do the work of checking if a parameter has been supplied, but a caller may omit the argument without running into trouble.

The key is to place a question mark behind the parameter. Let's look at an example:

JavaScript
setupFrames(fps: number, frames: number, rewind: boolean, id?: string) {
	if (id) {
		if (this.frameID === id)
			return true;
		
		this.frameID = id;
	}
	
	// ...
	return false;
}

Obviously we allow calling the method without specifying the id parameter. Therefore we need to check if it exists. This is done in the first line of the method's body. This guard protects the usage of the optional parameter, even though TypeScript allows us to use it at free will. Nevertheless, we should be careful. TypeScript won't detect all mistakes - it's still our responsibility to ensure a working code in every possible path.

Overloads

JavaScript by its nature does not know function overloads. The reason is quite simple: Naming a function only results in a local variable. Adding a function to an object places a key in its dictionary. Both ways allow only unique identifiers. Otherwise we would be allowed to have two variables or properties with the same name. Of course there is an easy way around this. We create a super function that calls sub functions depending on the number and types of the arguments.

Nevertheless, while inspecting the number of arguments is easy, getting the type is hard. At least with TypeScript. TypeScript only knows / keeps the types during compile-time, and then throws the whole created type system away. This means that no type checking is possible during runtime - at least not beyond very elementary JavaScript type checking.

Okay, so why is a subsection dedicated to this topic, when TypeScript does not help us here? Well, obviously compile-time overloads are still possible and required. Many JavaScript libraries offer functions that offer one or the other functionality, depending on the arguments. jQuery for instance usually offers two or more variants. One is to read, the other to write a certain property. When we overload methods in TypeScript, we only have one implementation with multiple signatures.

Typically one tries to avoid such ambiguous definitions, which is why there is are no such methods in the original code. We do not want to introduce them right now, but let's just see how we could write them:

JavaScript
interface MathX {
    abs: {
        (v: number[]): number;
        (n: number): number;
    }
}

The implementation could look as follows:

JavaScript
var obj: MathX = {
	abs: function(a) {
		var sum = 0;

		if (typeof(a) === 'number')
			sum = a * a;
		else if (Array.isArray(a))
			a.forEach(v => sum += v * v);

		return Math.sqrt(sum);
	}
};

The advantage of telling TypeScript about the multiple calling versions lies in the enhanced UI capabilities. IDEs like Visual Studio, or text editors like Bracket may show all the overloads including the descriptions. As usual calls are restricted to the provided overloads, which will ensure some safety.

Generics

Generics may be useful to tame multiple (type) usages, as well. They work a little bit different than in C#, as well, since they are only evaluated during compile time. Additionally they do not have anything special about the runtime representation. There is no template meta programming or anything here. Generics is only another way to handle type safety without becoming too verbose.

Let's consider the following function:

JavaScript
function identity(x) {
	return x;
}

Here the argument x is of type any. Therefore the function will return something of type any. This may not sound like a problem, but let's assume the following function invocations.

JavaScript
var num = identity(5);
var str = identity('Hello');
var obj = identity({ a : 3, b : 9 });

What is the type of num, str and obj? They might have an obvious name, but from the perspective of the TypeScript compiler, they are all of type any.

This is where generics come to rescue. We can teach the compiler that the return type of the function is the calling type, which should be of the exact type that has been used.

JavaScript
function identity<t>(x: T): T {
	return x;
}
</t>

In the above snippet we simply return the same type that already entered the function. There are multiple possibilities (including returning a type determined from the context), but returning one of the argument types is probably the most common.

The current code does not have any generics included. The reason is simple: The code is mostly focused on changing states and not on evaluating input. Therefore we mostly deal with procedures and not with functions. If we would use functions with multiple argument types, classes with argument type dependencies or similar constructs, then generics would certainly be helpful. Right now everything was possible without them.

Modules

The final touch is to decouple our application. Instead of referencing all the files, we will use a module loader (e.g. AMD for browsers, or CommonJS for node) and load the various scripts on demand. There are many advantage to this pattern. The code is much easier to test, debug and usually does not suffer from wrong orders, as the modules are always loaded after the specified dependencies are available.

TypeScript offers a neat abstraction over the whole module system, since it provides two keywords (import and export), which are transformed to some code that is related to the desired module system. This means that a single code base can be compiled to AMD conform code, as well as CommonJS conform code. There is no magic required.

As an example the file constants.ts won't be referened any more. Instead, the file will export its contents in form of a module. This is done via:

JavaScript
export var audiopath = 'Content/audio/';
export var basepath  = 'Content/';

export enum Direction {
	none  = 0,
	left  = 1,
	up    = 2,
	right = 3,
	down  = 4,
};

/* ... */

How can this be used? Instead of having a reference comment, we use the require() method. To indicate that we wish to use the module directly, we do not write var, but import. Please note, that we can skip the *.ts extension. This makes sense, since the file will have the same name later on, but a different ending.

JavaScript
import constants = require('./constants');

The difference between var and import is quite important. Consider the following lines:

JavaScript
import Direction = constants.Direction;
import MarioState = constants.MarioState;
import SizeState = constants.SizeState;
import GroundBlocking = constants.GroundBlocking;
import CollisionType = constants.CollisionType;
import DeathMode = constants.DeathMode;
import MushroomMode = constants.MushroomMode;

If we would write var, then we would actually use the JavaScript representation of the property. However, we want to use the TypeScript abstraction. The JavaScript realization of Direction is only an object. The TypeScript abstraction is a type, that will be realized in form of an object. Sometimes it does not make a difference, however, with types such as interfaces, classes or enums, we should prefer import to var. Otherwise we just use var for renaming:

JavaScript
var setup = constants.setup;
var images = constants.images;

Is this everything? Well, there is much to be said about modules, but I try to be brief here. First of all, we can use these modules to make interfaces to files. For instance the public interface to the main.ts is given by the following snippet:

JavaScript
export function run(levelData: LevelFormat, controls: Keys, sounds?: SoundManager) {
	var level = new Level('world', controls);
	level.load(levelData);

	if (sounds)
		level.setSounds(sounds);

	level.start();
};

All modules are then brought together in some file like game.ts. We load all the dependencies and then run the game. While most modules are just objects bundled together with single pieces, a module can also be just one of these pieces.

JavaScript
import constants = require('./constants');
import game = require('./main');
import levels = require('./testlevels');
import controls = require('./keys');
import HtmlAudioManager = require('./HtmlAudioManager');

$(document).ready(function() {
	var sounds = new HtmlAudioManager(constants.audiopath);
	game.run(levels[0], controls, sounds);
});

The controls module is an example for a single piece module. We achieve this with a single statement such as:

JavaScript
export = keys;

This assigns the export object to be the keys object.

Let's see what we got so far. Due to the modular nature of our code we included some new files.

TypeScript restructure

We have another dependency on RequireJS, but in fact our code is more robust and easier to extend than before. Additionally all dependencies are always exposed, which removes the possibility of unknown dependencies drastically. The module loader system combined with intellisense, improved refactoring capabilities and the strong typing added much safety to the whole project.

Of course not every project can be refactored so easily. The project has been small and was based on a solid code base, that did not rust that much.

In a final step we will break apart the massive main.ts file, to create small, decoupled files, which may only depend on some setting. This setting would be injected in the beginning. However, such a transformation is not for everyone. For certain projects it might add too much noise than gain clearity.

Either way, for the Matter class we would have the following code:

JavaScript
/// <reference path="def/jquery.d.ts"/>
import Base = require('./Base');
import Level = require('./Level');
import constants = require('./constants');

class Matter extends Base {
	blocking: constants.GroundBlocking;
	level: Level;

	constructor(x: number, y: number, blocking: constants.GroundBlocking, level: Level) {
		this.blocking = blocking;
		this.view = $('<div />').addClass('matter').appendTo(level.world);
		this.level = level;
		super(x, y);
		this.setSize(32, 32);
		this.addToGrid(level);
	}
	addToGrid(level) {
		level.obstacles[this.x / 32][this.level.getGridHeight() - 1 - this.y / 32] = this;
	}
	setImage(img: string, x: number = 0, y: number = 0) {
		this.view.css({
			backgroundImage : img ? img.toUrl() : 'none',
			backgroundPosition : '-' + x + 'px -' + y + 'px',
		});
		super.setImage(img, x, y);
	}
	setPosition(x: number, y: number) {
		this.view.css({
			left: x,
			bottom: y
		});
		super.setPosition(x, y);
	}
};

export = Matter;

This technique would refine the dependencies. Additionally the code base would gain accessibility. Nevertheless, it depends on the project and state of the code, if further refinement is actually desired or unnecessary cosmetics.

Using the code

Mario5 TypeScript

The code is live and available online at GitHub. The repository can be reached via github.com/FlorianRappl/Mario5TS. The repository itself contains some information on TypeScript. Additionally the build system Gulp has been used. I will introduce this build system in another post. Nevertheless, the repository also contains a short installation / usage guide, which should give everyone a jump start, who does not have knowledge about gulp or TypeScript.

Since the origin of the code lies in the Mario5 article, I also suggest everyone who has not read it, to have a look. The article is available on CodeProject at codeproject.com/Articles/396959/Mario. There is also a follow up article on CodeProject, which deals with extending the original source. The extension is a level editor, which showcases that the design of the Mario5 game has indeed been quite good as most parts of the UI can be easily re-used to create the editor. You can access the article under codeproject.com/Articles/432832/Editor-for-Mario. It should be noted that the article also deals with a social game platform that combines the game and the editor in a single webpage, which can be used to save and share custom levels.

Points of Interest

One of the most asked questions in the original article has been where to acquire the sound / how to set up the sound system. It turns out that the sound might be one of the most interesting parts, yet I decided to drop it from the article. Why?

  • The sound files might cause a legal problem (however, same could be said about the graphics)
  • The sound files are actually quite big (effect files are small, but background music is O(MB))
  • Every sound file has to be duplicated to avoid compatibility issues (OGG and MP3 files are distributed)
  • The game has been made independent of a particular sound implementation

The last argument is my key point. I wanted to illustrate that the game can actually work without strongly coupling it to a particular implementation. Audio has been a widely discussed topic for web applications. First of all we need to consider a series of formats, since different formats and encodings only work on a subset of browsers. To reach all major browsers, one usually needs at least 2 different formats (usually consisting of one open and one proprietary format). Additionally the current implementation of the HTMLAudioElement is not very efficient and useful for games. That is what motivated Google to work on another standard, which works much better for games.

Nevertheless, you want a standard implementation? The GitHub repository actually contains a standard implementation. The original JavaScript version is available, as is the Type'd version. Both are just called SoundManager. One is in the folder Original, the other one in the Scripts folder (both are subfolders of src).

History

  • v1.0.0 | Initial Release | 18.11.2014
  • v1.1.0 | Remark on TypeScript's history | 18.12.2014

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)