For several projects now I’ve been working with TypeScript as my primary front end development language.
In a few words TypeScript is a typed superset of JavaScript that can compile nicely typed code to javascript. When your front end applications get bigger and bigger with more and more functionality, type-safety is a great win. This is true for me in my own small projects, but even more true when your team of developers increase in numbers.
Getting your application properly typed might be a bit of a hazzle, you need to define your api-endpoints, what arguments do they accept, of what type? what do they return? what are the different states in that Enum that is used in my model? And once you’ve declared these interfaces you must keep them insync with your backend code as your application expands. That sucks.
About a year ago we started working with T4-codegeneration to get out of this manual work. This worked OK but was hard to customize to different needs in different applications.
6 months ago a collegue released TypeWriter, an extension to Visual Studio that works with the Code Dom to generate its content. Lets dive into some examples!
When TypeWriter is installed you can add a new TypeScript Template-item:
You will be presented with a default template like this:
1 2 3 4 5 6 7 8 9 10 11 |
module FantasticApplication.App { // $Classes/Enums/Interfaces(filter)[template][separator] // filter (optional): Matches the name or full name of the current item. * = match any, wrap in [] to match attributes // template: The template to repeat for each matched item // separator (optional): A separator template that is placed between all templates e.g. $Properties[public $name: $Type][, ] $Classes(Filter)[ export class $Name { } ] } |
TypeWriter applies its template to all relevant files. Lets say we have a file UserModel.cs
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace FantasticApplication.Models { public class UserModel { public int Id { get; set; } public string Name { get; set; } public UserState State { get; set; } public DateTime LastLoginDate { get; set; } } public enum UserState { Added, Deleted, Changed } } |
Replace this your template with this:
1 2 3 4 5 6 7 8 |
module App {$Classes(FantasticApplication.Models*)[ export interface $Name { $Properties[ $name: $Type] }]$Enums(FantasticApplication*)[ export enum $Name {$Values[ $name = $Value,] }] } |
A brief explanation of the code:
$Classes(FantasticApplication.Models*) [ ... ]
will iterate over all classes in UserModel.cs
whos fullname starts with FantasticApplication.Models
and output whats specified within the brackets.
$Name
is the name of the current context, might be a class-name, method-name or a property-name.
$name
is the same but with camelCase.
$Properties
is a collection of properties, output per property is specified within the brackets.
$Type
prints the type name.
When your UserModel.cs
is saved TypeWriter will re-evaluate the template and generate a nested file containing the output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module App { export interface UserModel { id: number name: string state: UserState lastLoginDate: Date } export enum UserState { added = 0, deleted = 1, changed = 2, } } |
Awesome! This means we have front-end code reflecting the models on out backend so that we can have a nice, typed, front end development experience for our models.
But how about controllers, you might ask?
Given that you have a Controller, say UserController.cs
like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace FantasticApplication.Controllers { public class UserController { [Route("users/query"), HttpPost] public JsonResult<List<UserModel>> Query() [...] [Route("users/get"), HttpPost] public JsonResult<UserModel> Get(int id) [...] [Route("users/update"), HttpPost] public JsonResult<UserModel> Update(UserModel model) [...] [Route("users/delete"), HttpPost] public JsonResult<UserModel> Delete(int id) [...] } } |
Add a new TypeScript-template item, Controllers.tst
and specify its content like this:
1 2 3 4 5 6 7 |
module App {$Classes(FantasticApplication.Controllers*)[ export class $Name { $Methods[ public $name = ($Parameters[$name: $Type][,]) : IPromise<$Type[$GenericTypeArguments[$Type]]> => { return $.post("$Route", { $Parameters[$name: $name][,] }); }] }] } |
Aaaand voila:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
module App { export class UserController { public query = () : IPromise<UserModel[]> => { return $.post("users/query", { }); } public get = (id: number) : IPromise<UserModel> => { return $.post("users/get", { id: id }); } public update = (model: UserModel) : IPromise<UserModel> => { return $.post("users/update", { model: model }); } public delete = (id: number) : IPromise<UserModel> => { return $.post("users/delete", { id: id }); } } } |
Thats it! Both your Models and your Controllers (returning your models) are now typed in your front-end code, yay!
This means intellisense when typing your application (like below) but above all: compile-time errors if types have changed on your backend!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
module App { export class SomeClassInNeedOfMyBackend { private users: UserModel[]; private user: UserModel; constructor(private userController: UserController) { this.userController.query() .then(u => this.users = u) .then(this.getUser) .then(this.setUserName) .then(this.deleteUser); } private getUser = () => { return this.userController.get(1).then(u => this.user = u); } private setUserName = () => { this.user.name = "Johan Nordin"; return this.userController.update(this.user); } private deleteUser = () => { return this.userController.delete(this.user.id); } } } |
This has surely increased out performance and quality when coding large javascript applications, hope it can help you aswell!
2 Responses
AngularJs: $resource vs $http - Nethouse Blog
[…] So this is fantastic, right? we have a tool for calling our backend that will give us typed parameters and results. But how about those parameters and results, who does the manual typing, making sure all parameters and all the result’s properties are accounted for? The solution is called TypeWriter and here is an earlier post about that: TypeScript codegeneration with Typewriter. […]
Angular & TypeScript: Typed interaction with your WebAPI-backend - Nethouse Blog
[…] written previously about angulars $http/$resource and about typescript codegeneration, now its time to write a few words about my current project and how we used TypeWriterand $http to […]