Hot off the press, TypeScript was announced earlier this week and has resulted in an explosion of commentary over Twitter and various blogs. The language claims to be tailored for application-scale development using JavaScript by providing a typed superset. It compiles to JavaScript so it will work in any browser. The promise is to make it easier to organize and structure code, as well as get rid of those annoying errors that leave users hunting through hundreds of lines of code seeking out an errant comma or issue with case sensitivity.
Ironically, I was speaking with an attendee at our Devscovery conference about the future of JavaScript right before TypeScript was announced. It’s obvious that JavaScript is here to stay, and it’s not going to get any better. It may get better parts, but backwards compatibility is going to force the bad parts to remain for a long time. What’s worse, there is nothing that convinces me browser vendors will play nice with the specifications when we’ve had a standard for almost 20 years and it is still implemented differently in different browsers. My take having come from a heavy JavaScript background before I was spoiled for several years by the ease of writing solid Silverlight business applications is that if we have to deal with it, we need to tackle it on three fronts:
- Education: JavaScript is a dynamic language where functions, not classes, rule. Developers need to stop trying to play object-oriented developer in the client space and learn how to play by the client rules.
- Moderation: Having said that, there are features that simply make it far too easy to shoot yourself in the foot. While there are decent solutions to address this like running JSHint/JSLint, I personally welcome an approach to catch issues at compile (or even better, development) time and keep us all honest. Bugs are easier to fix the closer to the source and the earlier in the process you are. One of the things I mentioned was that something like CoffeeScript should be implemented that helps protect us from the bad parts as we develop, but still generates to that friendly backwards-compatible JavaScript all of the browsers love.
- Better Glue. Yes, I said it. Silverlight developers know what I’m talking about *cough* RIA Services *cough* … now that we have very standard ways of exposing APIs, there should be easier ways to glue the client to the server so that developers are working on “what’s different” and not spending 75% of their time building controller methods and writing AJAX calls on the client.
I believe TypeScript is a tool that preserves the first item (it honors JavaScript and doesn’t try to hide the dynamic and flexible nature) and addresses the second item (it allows us to declare intent far more clearly and then have feedback right away) and probably does nothing for the last item.
Instead of debating whether or n
ot we need another language or if it is good or bad (and hey, I love C#, and I loved TurboPascal, so the fact that it was developed by Anders Hejlsberg doesn’t hurt) I would rather jump into using it so I can start to get a feel for how it helps or hurts. Tomorrow I’ll be speaking for three hours on this specific topic (how to leverage libraries in JavaScript development to help scale for large projects and with large teams) so I decided to grab an example I created with Backbone.JS to see what it would take to port over. Here we go!
Step one was simply implementing the VS 2012 plug-in. I’m sure I failed miserably at this. I get syntax highlighting but no template to add a new TypeScript file, no automatic compilation and no effective IntelliSense. I’m sure it’s operator error so stay tuned. You can grab whatever you need (or I should say, “Whatever is available”) from the TypeScript downloads link. Next, I added a JavaScript file and renamed it to have a .ts extension and added a post-build script to compile it:
tsc $(ProjectDir)/Scripts/app.ts
Next, I grabbed the language specification and went to work. First, I found I had to declare some variables to allow for external modules:
declare var $: any;
declare var _: any;
That was easy! Using Backbone however was a little more complicated. I cheated by taking a look at the existing TodoMVC sample from the TypeScript site. To allow Backbone to play nicely with TypeScript, I have to define a module and export classes that describe the API. It doesn’t have to be the full API, but can focus on the pieces I’ll be using. Here’s an example of exporting the Backbone Model object as a class:
declare module Backbone {
export class Model {
constructor (attr? , opts? );
get(name: string): any;
set(name: string, val: any): void;
set(obj: any): void;
save(attr? , opts? ): void;
destroy(): void;
bind(ev: string, f: Function, ctx?: any): void;
toJSON(): any;
}
}
Notice that I have optional parameters in the constructor. I can declare intention by providing a type for named parameters and the return type of the function. Pretty cool. I like to use Backbone.Events as well, and TypeScript allows me to define interfaces. Here is the interface for events specifying the signature for “on” and “off” (register and unregister):
interface HasEvents {
on: (event: string, callback: (parms?: any, moreParms?: any) => any) => any;
off: (event: string) => any;
}
The easiest way to read this in my opinion is right to left. A function that returns any type takes the parameters of, well, in the case of the first one, a function that has two optional parameters and returns any type, and the first parameter is a string. I also made an interface for the simple version of a contact to make it clear when I am expecting an instance of this:
interface ContactInterface {
id?: number;
firstName: string;
lastName: string;
email: string;
address: string;
city:
string;
state: string;
};
You can also declare static properties on classes, so I created a template class just to hold the compiled templates I am using:
class Templates {
static contact: (json: any) => string;
static editContact: (json: any) => string;
static contacts: (json: any) => string;
};
Note that the compiled template takes any type of object and returns a string. Finally, I created a global object to host cross-cutting events in lieu of using a pub/sub library like Postal. Note the implementation of the HasEvents interface.
class AppEvents implements HasEvents
{
on: (event: string, callback: (model: Contact, error: any) => any) => any;
off: (event: string) => any;
trigger: (event: string, data: any) => any;
};
Finally, you get to see some plain JavaScript – I create the object and extend it to have events:
var appEvents = new AppEvents();
_.extend(appEvents, Backbone.Events);
So now let’s tackle how we did it the “old way” vs. “the new way.” Here is the old way I defined my contact model:
App.Models.Contact = Backbone.Model.extend({
initialize: function () {
console.log(“Contact init.”);
},
defaults: function () {
return {
id: null,
firstName: ”,
lastName: ”,
email: ”,
address: ”,
city: ”,
state: ”
};
},
validate: function (attrs) {
if (_.isEmpty(attrs.firstName)) {
return “First name is required.”;
}
if (_.isEmpty(attrs.lastName)) {
return “Last name is required.”;
}
if (_.isEmpty(attrs.email)) {
return “Email is required.”;
}
if (attrs.email.indexOf(“@”) < 1 ||
attrs.email.indexOf(“.”) < 1) {
return “The email address appears to be invalid.”;
}
return “”;
}
});
Basically I’m logging something to the console, providing a template for a contact instance, and declaring my validations. Notice that there is no way to guarantee what I return from the function (I could just as easily send back { foo: “bar” }) and that the attri
butes for the validation could be anything as well. With TypeScript, I can be more specific and even generate development-time (and compile-time) errors if I’m not doing what I expect:
class Contact extends Backbone.Model implements HasEvents {
on: (event: string, callback: (model: Contact, error: any) => any) => any;
off: (event: string) => any;
isValid: () => bool;
initialize() {
console.log(“Contact init.”);
};
defaults() : ContactInterface {
return {
id: null,
firstName: ”,
lastName: ”,
email: ”,
address: ”,
city: ”,
state: ”
};
};
validate(attrs: ContactInterface) {
if (_.isEmpty(attrs.firstName)) {
return “First name is required.”;
}
if (_.isEmpty(attrs.lastName)) {
return “Last name is required.”;
}
if (_.isEmpty(attrs.email)) {
return “Email is required.”;
}
if (attrs.email.indexOf(“@”) < 1 ||
attrs.email.indexOf(“.”) < 1) {
return “The email address appears to be invalid.”;
}
return “”;
}
};
Notice that I implement the events in a more specific way, providing a detailed signature for the callback. Functions are simply declared similar to how they are defined on C# classes and I don’t have to worry about exposing the property name. Finally, I can type the return value and the parameters for the various functions. Pretty cool, no? I’m still using JavaScript and the object can still be dynamic, but I can at least constrain it to the specific interface I’m going to use within my method.
Here’s the old definition for the collection, passing in the options to configure it to point to the REST API on my server:
App.Collections.Contacts = Backbone.Collection.extend({
model: App.Models.Contact,
url: “/api/Contacts”,
initialize: function () {
console.log(“Contacts init.”);
}
});
Here’s the new definition that derives from the base class and uses a constructor instead. I also make it very clear I’m using events on the collection as well, which isn’t evident in the original JavaScript.
class Contacts extends Backbone.Collection
implements HasEvents {
on: (event: string, callback: (parms?: any, moreParms?: any) => any) => any;
off: (event: string) => any;
url: string;
model: Contact;
initialize() {
console.log(“Contacts init.”);
}
constructor(options?: any) {
this.url = “/api/Contacts”;
super(options);
};
};
Another cool feature is that I can declare types and then cast calls that aren’t apparent to a type. For example, I haven’t informed TypeScript that jQuery can return a DOM element, but on my declaration for views I do use the HTMLElement type. To reconcile this, I can simply cast the result like this:
this.el = <HTMLElement>$(“#contact”);
I know, it looks like an HTML tag but that is because it’s not a true cast, only a tip that this is the return type we expect. After several more models and views, I define an overall application object that spins up the initial collection in the constructor. Note how I also set the static template properties – very straightforward syntax. Creating the object kicks off the application:
class Application {
public static editContactView: EditContactView;
constructor() {
Templates.contact = _.template($(“#contactTemplate”).html());
Templates.editContact = _.template($(“#editContactTemplate”).html());
Templates.contacts = _.template($(“#contactsTemplate”).html());
var contacts = new Contacts();
contacts.fetch();
var contactsView = new ContactList({
collection: contacts
});
contactsView.render();
};
};
// host container
var App = new Application();
The application demonstrates CRUD with a list that populates a form with validation where the user can edit, delete, and add new contacts.
The build process compiles the TypeScript to a JavaScript file that is actually referenced from the MVC view. You won’t see interfaces or definitions because those help enforce things at development and compile time. What does the output look like? Here’s the compiled application-wide event object I created:
var AppEvents = (function () {
function AppEvents() { }
return AppEvents;
})();
; ;
var appEvents = new AppEvents();
_.extend(appEvents, Backbone.Ev
ents);
Here is the contact model definition – note how the Backbone Model is passed in for extension:
var Contact = (function (_super) {
__extends(Contact, _super);
function Contact() {
_super.apply(this, arguments);
}
Contact.prototype.initialize = function () {
console.log(“Contact init.”);
};
Contact.prototype.defaults = function () {
return {
id: null,
firstName: ”,
lastName: ”,
email: ”,
address: ”,
city: ”,
state: ”
};
};
Contact.prototype.validate = function (attrs) {
if(_.isEmpty(attrs.firstName)) {
return “First name is required.”;
}
if(_.isEmpty(attrs.lastName)) {
return “Last name is required.”;
}
if(_.isEmpty(attrs.email)) {
return “Email is required.”;
}
if(attrs.email.indexOf(“@”) < 1 || attrs.email.indexOf(“.”) < 1) {
return “The email address appears to be invalid.”;
}
return “”;
};
return Contact;
})(Backbone.Model);
Jeremy, where’s the full download? Sorry, right now the project is reserved for attendees at Devscovery – I may have a full project to make available after this week, but for now you can take a look at the full TypeScript source (this is the entire client application) by clicking here.
Thanks! What do I think? So far my thoughts are very positive. I see this being a useful tool that enables teams to write the code they are used to, with options to add safeguards and types where necessary to avoid errors and make the code easier to organize, read, and comprehend. I’ll have to use it some more but this short project of converting an application over has me raising both thumbs up.
Safeguard Your Business and Stay Operational Even When Disasters Strike
Disasters come in many forms and our reliance on technology to conduct business means we are vulnerable when any event negatively impacts systems and applications. From cyber attacks and employee mistakes to power outages and earthquakes, there are many forms to events which will result in a potential disaster situation.
Assess Areas of Risk
Together we identify all data and backup needs along with acceptable levels of reliability.
Protect Systems and Data
You can rely on our extensive experience deploying solutions that meet necessary recovery objectives.
Use Proven Playbooks
Together we maintain comprehensive documentation and set of procedures taken before, during and after a disaster.