Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backbone and TypeScript, an unhappy marriage: Building a type-safe "get"?

I am trying to use TypeScript with Backbone.js. It "works", but much of the type safety is lost by Backbone's get() and set(). I am trying to write a helper method that would restore type-safety. Something like this:

I'd put this in my model:

object() : IMyModel  {
    return attributes; // except I should use get(), not attributes, per documentation
}

And this in the consumer: var myVar = this.model.object().MyProperty;

With this syntax, I get TypeScript's knowledge that MyProperty exists and is bool, which is awesome. However, the backbone.js docs tell me to use get and set rather than the attributes hash directly. So is there any magic Javascript way to pipe usage of that object through get and set properly?

like image 594
Scott Stafford Avatar asked Mar 08 '13 16:03

Scott Stafford


2 Answers

We are using backbone with TypeScript heavily, and have come up with a novel solution.
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');
    }
    set Description(value: string) {
        this.set('Description', value);
    }
    get Description(): string {
        return this.get('Description');
    }

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

Note that the interface defines the properties of the model, and the constructor ensures that any object passed will have the Id, Name and Description properties. The for statement simply calls backbone set on each property. Such that the following test will pass:

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

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

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

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

Update: I have updated the code base to use ES5 get and set syntax, as well as the constructor. Basically, you can use the Backbone .get and .set as internal variables.

like image 126
blorkfish Avatar answered Nov 08 '22 13:11

blorkfish


I've come up with the following using generics and ES5 getters/setters, building off of the /u/blorkfish answer.

class TypedModel<t> extends Backbone.Model {
    constructor(attributes?: t, options?: any) {
        super(attributes, options);

        var defaults = this.defaults();
        for (var key in defaults) {
            var value = defaults[key];

            ((k: any) => {
                Object.defineProperty(this, k, {
                    get: (): typeof value => {
                        return this.get(k);
                    },
                    set: (value: any) => {
                        this.set(k, value);
                    },
                    enumerable: true,
                    configurable: true
                });
            })(key);
        }
    }

    public defaults(): t {
        throw new Error('You must implement this');
        return <t>{};
    }
}

Note: Backbone.Model defaults is optional, but since we use it to build the getters and setters, it is now mandatory. The error that is thrown forces you to do this. Perhaps we can think of a better way?

And to use it:

interface IFoo {
    name: string;
    bar?: number;
}

class FooModel extends TypedModel<IFoo> implements IFoo {
    public name: string;
    public bar: number;

    public defaults(): IFoo {
        return {
            name: null,
            bar: null
        };
    }
}

var m = new FooModel();
m.name = 'Chris';
m.get('name'); // Chris
m.set({name: 'Ben', bar: 12});
m.bar; // 12
m.name; // Ben

var m2 = new FooModel({name: 'Calvin'});
m2.name; // Calvin

It's slightly more verbose than ideal, and it requires you to use the defaults, but it works well.

like image 29
Chris MacDonald Avatar answered Nov 08 '22 15:11

Chris MacDonald