Reputation: 44798
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?
Upvotes: 12
Views: 8770
Reputation: 33
I am struggling with the same issue but I guess I found and interesting solution with the from TypeScript chatgroup. The solution seems pretty promising and I would like to share it here. So my code now looks like this
//Define model structure
interface IMarkerStyle{
Shape:string;
Fill:string;
Icon:string;
Stroke:string;
};
export class MarkerStyle extends StrongModel<IMarkerStyle>{
//Usage
let style=new MarkerStyle();
//Most interesting part. Oddly enough thease lines check for type
style.s("Fill","#F00"); //setter OK: Fill is defined as string
style.s("Fill",12.3); //setter ERROR: because of type mismatch
Another benefit I got is it checks defaults and constructor parameters for complience with the interface. So static type checking will not allow you to specify a default value for a property that does not exist
let style=new MarkerStyle(
{
Shape:"circle", //OK
Phill:"#F00", //ERROR typo in field name
Icon:"car" //OK
//ERROR Stroke is not optional in interface and not specified here
}
);
Upvotes: 0
Reputation: 4099
Here is a way using decorators, create a base class like this:
export class Model<TProps extends {}> extends Backbone.Model {
static Property(fieldName: string) {
return (target, member, descriptor) => {
descriptor.get = function() {
return this.get(fieldName);
};
descriptor.set = function(value) {
this.set(fieldName, value);
};
};
}
attributes: TProps;
}
Then create your own classes like this:
class User extends Model<{id: string, email: string}> {
@Model.Property('id') set Id(): string { return null; }
@Model.Property('email') set Email(): string { return null; }
}
And use it:
var user = new User;
user.Email = '[email protected]';
console.log(user.Email);
Upvotes: 0
Reputation: 22844
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.
Upvotes: 17
Reputation: 6126
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.
Upvotes: 10