Writing Backbone Applications in TypeScript

TypeScript

Nowadays JavaScript evolves fast. We have ECMAScript 6th and 7th editions released over just 2 years. the current support of ES6 specification in major browsers is close to 100%. So the language changed, and changed for better. It has become more expressive, much more concise and readable. It is tempting to jump into, but what about older browsers? Transpilers can translate ES.Next into the old-good JavaScript of ECMAScript 5th edition.

Then the question is which transpiler to pick up? Babel (https://babeljs.io/) is still on the rise, but look what is happening to TypeScript. Since it was chosen for Angular2 it is gaining its momentum. Why have they decided for TypeScript? Because it’s much more than transpiler. TypeScript enhances JavaScript with optional features such as static typing, interfaces, abstract classes, member visibility and more. That’s a completely new level, not available in plain JavaScript. For example if have a simple function accepting a string for trimming:

function trim( text ) {
  return text.trim();
}

If we accidently pass not a string it throws an obscure TypeError

trim( 1 ); // TypeError: text.trim is not a function
trim({}); // TypeError: text.trim is not a function

It’s even worse if the function accepts wrong typed arguments. It may product in a not-intended result, what can be hard to trace. In both cases we are forced to extend the function with lines and lines validating the input.

On the contrary with TypeScript we can simply add the type constraints on both entry and exit points:

function trim( text: string ): string {
  return text.trim();
}

As soon as you dare to give a wrong type you get warned, already on the IDE level.

Writing Backbone Applications in TypeScript

Thus we can establish strict interfaces and afterwards TypeScript takes care that they are not violated. In addition to consistency it saves enormous amount of time on debugging. We get immediately informed where exactly and what we did wrong.

It’s all nice, but being a Backbone developer you may think of how it may help you. It may indeed. TypeScript is quite friendly to external libraries. You just need to enable the corresponding typings that can be found in public repository https://github.com/DefinitelyTyped/DefinitelyTyped

Setting up the development environment

First of all we need some setting up. If we do not have TypeScript compiler we install it with npm:

npm install -g typescript

We will need also typings tool to manage TypeScript definition dependencies, e.g. for Backbone

npm install typings --global

Now we can create a sandbox directory, enter it and install the typings:

typings install dt~jquery --global --save
typings install dt~underscore --global --save
typings install dt~backbone-global --global --save

It creates typings directory and the index file index.d.ts in it. Whenever we add new typings they get automatically included in the index.

We also place in our directory tsconfig.json with the desired TypeScript compiler configuration:

{
  "compilerOptions": {
    "target": "ES5",
    "module": "commonjs",
    "moduleResolution": "node",
    "outDir": "build/"
  },
  "files": [
    "./typings/index.d.ts",
    "app/bootstrap.ts"
  ]
}

We ask here TypeScript to convert our code in ES5 (an old edition currently supported by almost every user-agent). We state that our modules to translate into CommonJS and to resolve in Node.js style. At last we require TypeScript to output the generated ES5-code into build directory and to start app/bootstrap.ts module. It shall also include the installed typings (./typings/index.d.ts).

Well, TypeScript will look for bootstrap.ts in app directory. So we have to place there our first demo module. For example:

console.log( "Hello world!" );

In order to send it into a browser we compose our index.html

<!doctype html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>

  <div id="demo"></div>

  <!-- Backbone  -->
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>

  <!-- Module loader -->
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.38/system.js"></script>


  <script>
    System.config({
      packages: {
        ".": {
          format: "cjs",
          defaultExtension: "js"
        }
      }
    });
    System.import( "./build/bootstrap" );
  </script>

	</body>
</html>

Remember TypeScript is required to produce CommonJS modules. Here we load them asynchronously with SystemJS, but we could bundle the modules into a single file with Browserify and load it as script element source.

At that point our working directory may look like that:

Writing Backbone Applications in TypeScript

Everything is ready, so we compile our app/bootstrap.ts:

tsc

It compiles our source into build/bootstrap.js and in this case simply coping the content of bootstrap.ts. As we request index.html from a browser we find in the JavaScript console the desired Hello world!

If we put in app/bootstrap.ts a function with type constraints

function bracify( text: string ): string {
  return "{" + text + "}";
}

var bracified = bracify( "Hello world!" )
console.log( bracified );

the generated build/bootstrap.js will have the same function but without them:

function bracify(text) {
    return "{" + text + "}";
}
var bracified = bracify("Hello world!");
console.log(bracified);

If rewrite the code in ES6 syntax

let bracify = ( text: string ): string => {
  return `{${text}}`;
}

let bracified = bracify( `Hello world!` )
console.log( bracified );

we miraculously get in build/bootstrap.js the same content as in the previous example.

TypeScript compiles gracefully, generating readable JavaScript, extending it with extra code only where it’s necessary.

Creating a view

Let’s now create a simple Backbone view. It’ll be a form to add a super-hero. For that we need a module. We can go with old-good CommonJS, but we rather do as it recommended in ES6 specification

app/bootstrap.ts

import { HeroView } from "./view/hero";

new HeroView({
  el: "#demo"
});

Here we import the view class from the module ./view/hero. The module content then looks like that:

export class HeroView extends Backbone.View<Backbone.Model> {
  private static tpl = `
    <form>
      <input name="name" placeholder="Name..." />
      <button type="submit">Submit</button>
    </form>
  `;
  initialize(){
    this.render();
  }
  render(){
    this.$el.html( HeroView.tpl );
    return this;
  }
}

We declare a view class that inherits from Backbone.View. By the requirements of backbone-global typings we have to explicitly specify the model class, which the view accepts. In this case we use no model so the generics may have Backbone.Model. The class consists of two public methods (initialize and render) and a static property tpl. As you might noted the short function notation doesn’t require keyword function. When visibility modifier (public, private, protected) is omitted the member is considered as public. You know the method initialize Backbone calls during construction. So we simply require our view to render automatically. And for the rendering it replaces the bounding element’s innerHTML with content of tpl property.

Note, that we’ve used here Template Literal. It simplifies the assignment of the multiline string plus evaluates expressions within it.

Writing Backbone Applications in TypeScript

But what if we want to reference the options passed to the view constructor? The most obvious way to achieve it is declare a property and set during construction.

export class HeroView extends Backbone.View<Backbone.Model> {
  private options: Backbone.ViewOptions<Backbone.Model>;
  initialize( options: Backbone.ViewOptions<Backbone.Model> ){
    this.options = options;
  }
}

It looks too verbose, doesn’t it? TypeScript allows shorter notation:

export class HeroView extends Backbone.View<Backbone.Model> {
  initialize( private options: Backbone.ViewOptions<Backbone.Model> ){
  }
}

Adding a collection

Well we have a control to type in a super-hero name. It would be nice to accompany it with hero super-power. So we enlist the options in

powers.json file:

 [
  "Superhuman senses",
  "Superhuman strength",
  "Wallcrawling",
  "Waterbreathing"
]

Each entity of the list we will wrap into a model declared in model/heropower.ts:

export class HeroPowerModel extends Backbone.Model {
  parse( data: string ){
    return {
      item: data
    };
  }
}

This model will be consumed by the collection collection.heropower.ts

import { HeroPowerModel } from "../model/heropower";

export class HeroPowerCollection extends Backbone.Collection<HeroPowerModel> {
  url= "./powers.json";
  model = HeroPowerModel;
}

So the collection fetches powers.json, converts items into HeroPowerModel instances and exposes API to handle the list.

Eventually we modify the view (view/hero.ts) to build the list of select options from the passed collection:

export class HeroView extends Backbone.View<Backbone.Model> {
  private static tpl = `
    <form>
      <input name="name" placeholder="Name..." />
      <select name="power">
        <% for(var inx in powers) { %>
            <option><%= powers[inx].item %></option>
        <% } %>
       </select>
      <button type="submit">Submit</button>
    </form>
  `;
  private template: (...data: any[]) => string;
  initialize(){
    this.template = _.template( HeroView.tpl );
    this.collection.fetch({
      success: () => {
        this.render();
      }
    });
  }
  render(){
    let html = this.template({ powers: this.collection.toJSON() });
    this.$el.html( html );
    return this;
  }
}

Here we compile our template with _.template into this.template. On rendering we use the last one to build a new state of view based on the current content of the collection.

Writing Backbone Applications in TypeScript

Note that we’ve used fat arrow function notation for the success handler of the collection fetch method.

success: () => {
  this.render();
}

The reason was not about shortening the code (with short function notation it could be even more concise). Fat arrows functions maintain the outer context, meaning within the function body we can courageously refer to this knowing it’s our HeroView instance.

While speaking of functions we can examine rest and spread operators.

function log( message: string, ...args: any[] ) {
  console.log( message, JSON.stringify( args ) );
}
log( "show me", 1, 2, 3 ); // show me [1, 2, 3]

The first allows us to receive all or part of passed arguments as an array without any tricks on arguments object.

function save( foo, bar, baz  ) {
}
let csv = "foo,bar,baz".split( "," );
save( ...csv );

The second on the contrary spreads a given array into a list of arguments.

Give it some live…

What about handling DOM events? For example we can make the form reacting to submit event:

export class HeroView extends Backbone.View<Backbone.Model> {
  // ...
  events(){
    return <Backbone.EventsHash> {
      "submit form": "onSubmit"
    }
  }


  onSubmit( e: Event ){
    e.preventDefault();
    alert( "Form submitted" )
  }
}

According to Backbone.View interface events must return Backbone.EventsHash interface. So we can use type assertion to cast this type on the return object-literal.

Another case were need type assertion is bringing a broader abstract interface to the concrete desired.

export class HeroView extends Backbone.View<Backbone.Model> {
  // ...
  events(){
    return <Backbone.EventsHash> {
      "submit form": "onSubmit",
      "change select": "onSelect"
    }
  }
  onSelect( e: Event ){
    let el = <HTMLSelectElement> e.target;
    console.log( el.value );
  }
}

Method onSelect receives an argument of type Event that has a property of type EventTarget. So you cannot access for example e.target.value as this property doesn’t exist in the interface. But we know that our EventTarget is always a select control therefore of HTMLSelectElement type. So we have to inform TypeScript about it.

Another interesting topic regarding ES6 and TypeScript is destructuring. When we have an array-like object we can destructure it into a list of variables as easy as that:

  onSelect( e: Event ){
    let [ first, second ] = this.$( "option" ).toArray();
    console.log( first, second );
  }

Destruturing is also available for objects. Similar to array elements we can extract object members:

  onSelect( e: Event ){
    let el = <HTMLInputElement> e.target;
    let { classList } = el;
    classList.add( "foo" );
    classList.add( "bar" );
    console.log( el.className ); // foo bar
  }

Custom interfaces

Coming back to out HeroView example, we set there quite a broad interface for the compiled template function:

private template: (...data: any[]) => string;

This notation means that we can pass any number of any types to the function, what is not true in reality. Let’s create a proper interface for that.

interface Template {
  ( data: TemplateData ): string;
}

First of all we declare the interface for the function. It is expected to take the only argument of the type TemplateData and produce a string. The spec for TemplateData type may look like that:

interface TemplateData {
  powers: { [ inx: number ]: PowerModelJson };
}

It’s an array of PowerModelJson types where every element described with the corresponding interface:

interface PowerModelJson {
  item: string;
}

So that’s an object literal with one property item of type string.

What’s left to do is to change the type of th view template property:

private template: Template;

Since now on an attempt to pass invalid type TypeScript warns us

Writing Backbone Applications in TypeScript

Afterwords

I had no intend to span all the aspects of bringing Backbone onto TypeScript ground. It would be impossible in boundaries of an article. Instead I wanted to show that is no rocket science switching to TypeScript, but advantages of such move cannot be overestimated. Here I give a few sources that may help you on the start.