TypeScript strongly typed Backbone Models

TypeScript and Backbone are a great fit in terms of writing simple, Object-Oriented code using JavaScript.  The simplicity of Backbone, coupled with the TypeScript generated closure syntax allow one to simply use the TypeScript extends keyword to derive from any of Backbone’s base classes – and start implementing functionality immediately.  This is a very natural way to write Object-Oriented JavaScript:

class ListItem extends Backbone.Model {

}

The problem with Backbone Models

Unfortunately, Backbone uses object attributes to store Model properties, and these need to be set in order for the Backbone model to work correctly.  The following code shows how to set Backbone Model properties:

class ListItem extends Backbone.Model {
    constructor() {
        super();
        this.set('Id', '2');
    }
}

The nature of the set and get functions of Backbone, however do not have any inherent type-safety.  These model properties are also not exposed as first-class object properties, and must always be accessed via the setter and getter functions.  A more natural way of expressing Backbone Models would be as follows:

class ListItem extends Backbone.Model {
    Id: number;
    Name: string;
    constructor() {
        super();
        this.Id = 2;
        this.Name = "ModelName";
    }
}

Using ES5 getter and setter syntax

The above effect can be achieve by using ES5 getter and setter syntax.  By defining a get and set function for each property, and then in turn calling the Backbone get and set functions, we can keep our Backbone Model in-synch with our TypeScript properties.

Note that you will need to switch your TypeScript project properties to compile to ES5 in order for the following code to work:

class ListItem extends Backbone.Model {
    get Id(): number {
        return this.get('Id');
    }
    set Id(value: number) {
        this.set('Id', value);
    }
    set Name(value: string) {
        this.set('Name', value);
    }
    get Name(): string {
        return this.get('Name');
    }

    constructor() {
        super();
        this.Id = 1;
        this.Name = "ModelName";
    }
}

Model Type-safety

We can further improve our model’s type-safety by using an interface – both for the implements keyword ( forcing our class to implement getters and setters for each interface property – and also for the object constructor.

Consider the following code:

interface IListItem {
    Id: number;
    Name: string;
    Description: string;
}

class ListItem extends Backbone.Model implements IListItem {
    get Id(): number        { return this.get('Id'); }
    set Id(value: number)   { this.set('Id', value); }
    set Name(value: string) { this.set('Name', value); }
    get Name(): string      { return this.get('Name'); }

    constructor(input: IListItem) {
        super();
        this.Id = input.Id;
        this.Name = input.Name;
    }
}

Note that we have defined an interface ( IListItem ), and forced the ListItem object to implement the interface.  We have also added the interface to the constructor, further enhancing our type-safety.

This ensures that changes to the interface will generate compile-time errors if the object does not have corresponding getter and setter functions, and also ensures that any object passed via the constructor will have all properties defined.

Simplifying the constructor

For a Backbone model with many properties, the constructor can further be simplified by looping through the properties of the input parameter as follows:

interface IListItem {
    Id: number;
    Name: string;
}

class ListItem extends Backbone.Model implements IListItem {
    get Id(): number { return this.get('Id'); }
    set Id(value: number) { this.set('Id', value); }
    set Name(value: string) { this.set('Name', value); }
    get Name(): string { return this.get('Name'); }

    constructor(input: IListItem) {
        super();
        for (var key in input) {
            if (key) {
                this[key] = input[key];
            }
        }
    }
}

A Jasmine Unit test

The above code will allow for both standard get and set Backbone syntax, as well as type-safe TypeScript syntax as shown in the following unit test:

describe("SampleApp : tests : models : ListItem_tests.ts ", () => {
    it("can construct a ListItem model", () => {
        var listItem = new ListItem(
            {
                Id: 1,
                Name: "TestName",
            });
        expect(listItem.get("Id")).toEqual(1);
        expect(listItem.get("Name")).toEqual("TestName");

        expect(listItem.Id).toEqual(1);

        listItem.Id = 5;
        expect(listItem.get("Id")).toEqual(5);

        listItem.set("Id", 20);
        expect(listItem.Id).toEqual(20);
    });

});

Note how a List Item is constructed with a standard JavaScript object definition.  Also, both listItem.Id or listItem.set(‘Id’, 20) syntax can be used to interact with a type-safe TypeScript Backbone model.

Have fun,

blorkfish.

Note : This blog post was as a result of a question posted on stackoverflow.com

Advertisements