Friday, August 21, 2015

Sharing TypeScript classes between front-end and web server code

The beauty of using javascript is it can now run on server-side via nodejs. Using javascript on both client-side and server-side facilitates code-sharing much easier.

Prerequisite: use UMD.
{
 "compilerOptions": {
  "target": "ES5",
  "module": "umd",
  "sourceMap": true
 } 
}

This is a class with validation logic that can be used on front-end:

/shared/Domain/Person.ts
module Domain {
        
    export class Person {
        name : string;
        age : number;
        
        validate() : string[] {
            var validations : string[] = [];
            
            if (this.age < 0 || this.age == undefined || this.age == null)
                validations.push('Age must be equal or more than zero');
                
            return validations;    
        }
        
    }
}




The above can be used directly on front-end app:

/public/app/Person/Controller.ts
class Controller {
    
    name : string = 'Linus T';
    
    person  = new Domain.Person();
    
    validationMessages : string[];
        
    constructor() {
        this.person.name = 'Nice fellow';
        this.person.age = -39;     
    } 
    
    save() : void {
                
        this.validationMessages = this.person.validate();
                
    }
                
}

angular.module('TheApp', []).controller('Controller', Controller);


The javascript on the browser don't have a problem looking for the reference of Domain.Person class, as the class is referenced from the view.
<script src='/angular/angular.min.js'></script>
<script src='/shared/Domain/Person.js'></script>
<script src='Controller.js'></script>

<div ng-app="TheApp" ng-controller="Controller as c">
    {{c.name}}
    
    <p>
        Person: {{c.person}}
    </p>    
    
    <p>
        {{c.validationMessages}}
    </p>
    
    <button ng-click="c.save()">Save</button>
    
</div>


However, you cannot import an internal module directly from a nodejs app. TypeScript complains that a module is not a module (external module that is).



In order to use a module in nodejs, the module must be an external one. To convert an internal module to external module, just export the module Domain by attaching export = Domain at the end of code.


/shared/Domain/Person.ts
module Domain {
        
    export class Person {
        name : string;
        age : number;
        
        validate() : string[] {
            var validations : string[] = [];
            
            if (this.age < 0 || this.age == undefined || this.age == null)
                validations.push('Age must be equals or more than zero');
                
            return validations;    
        }
        
    }
}

export = Domain;

Doing the above, this shall work.

/app.ts
import domain = require('./shared/Domain/Person');

app.post('/api/person', (req,res) => {
    
    var person = new domain.Person(); // intellisense works
    extend(person, req.body);
            
    var messages = person.validate();  
  
      if (messages.length > 0)
        res.status(400).json(messages);
    else
        res.send('OK'); 



And also, another problem is once you convert an internal module to external module, the module can't be recognized as an internal one anymore. Hence this front-end code will get an error:




Fortunately, someone was able to crack that problem. Another dev make it clearer. There's another dev who made a post on his enlightenment on the internals of require function.


To cut to the chase, keep the module as internal, then manually register modules to exports variable (the global variable being used by CommonJS's require).


/shared/Domain/Person.ts:
module Domain {
        
    export class Person {
        name : string;
        age : number;
        
        // we can use this logic on front-end, e.g., Angular.
        // and we can also re-use this logic on back-end, e.g., NodeJS's REST API 
        validate() : string[] {
            var validations : string[] = [];
            
            if (this.age < 0 || this.age == undefined || this.age == null)
                validations.push('Age must be equals or more than zero');
                
            return validations;    
        }
        
    }
}

declare var exports: any;
if (typeof exports != 'undefined') {
    exports.DomainPerson = Domain.Person;


Organize the classes to their own file, let's add another one. The exports code have to be repeated on each class.
module Domain {
    export class Country {
        name : string;
    }
}

declare var exports: any;
if (typeof exports != 'undefined') {
    exports.DomainCountry = Domain.Country;
}


Then on external module-using code, we will not use TypeScript's import functionality, so the TypeScript compiler will not flag the module as not a module. To wit:


class ExternalizedDomain {
    static Person : typeof Domain.Person = require('./shared/Domain/Person').DomainPerson;
    static Country : typeof Domain.Country = require('./shared/Domain/Country').DomainCountry;
}


The suffixes .DomainPerson and .DomainCountry are the name of the exported classes from modules that were stored in exports variable, which in turn is returned by the require function. As the module is not imported by TypeScript, the modules would have no associated types, we can re-introduce the imported modules as strongly-typed classes by using TypeScript's typeof operator.

Following is the REST API code that re-uses the same class(with validation logic) being used by front-end. Using nodejs, we can keep our front-end code and REST API code DRY.


import express = require('express');

import path = require('path');

import bodyParser = require('body-parser'); 

import extend = require('extend');


class ExternalizedDomain {
    static Person : typeof Domain.Person = require('./shared/Domain/Person').DomainPerson;
    static Country : typeof Domain.Country = require('./shared/Domain/Country').DomainCountry;
}


var app = express();


app.get('/', (req,res) => res.send('Hello Lambda World!'));

    var server = app.listen(3000, function () {
    var host = server.address().address;
    var port = server.address().port;

    console.log('Example app listening at http://%s:%s', host, port);
    
});



app.use('/', express.static( path.join(__dirname, 'public'), { extensions: ['html'] })); // if entered a url without an extension, attach html
app.use('/angular', express.static( path.join(__dirname, 'node_modules', 'angular') )); 

app.use('/shared', express.static( path.join(__dirname, 'shared') )); 

app.use(bodyParser.json());
app.post('/api/person', (req,res) => {
   
   
    var person = new ExternalizedDomain.Person();
    extend(person, req.body);
            
    var messages = person.validate();  
   
    if (messages.length > 0)
        res.status(400).json(messages); // 400 is http status for bad request
    else
        res.send('OK'); 
    
});


The technique above works on modules too:
module Domain.Calculator {

    export function multiply(multiplicand: number, multiplier: number): number {

        return multiplicand * multiplier ;
    }

    export function divide(dividend: number, divisor: number): number {

        return null;
    }
}

declare var exports: any;
if (typeof exports != 'undefined') {
    exports.Calculator = Domain.Calculator;
}

To use:

class ExternalizedDomain {
    static Calculator : typeof Domain.Calculator = require('../shared/Domain/Calculator').Calculator;
}

describe("multiplication", () => {
    it("should multiply 2 and 3", () => {
        var product = ExternalizedDomain.Calculator.multiply(2,3);
        expect(product).toEqual(6);
    });
});


TypeScript works perfect on Visual Studio Code. Intellisense works as expected:



WebStorm can't intelligently work on the language:




Complete code, done on Visual Studio Code: https://github.com/MichaelBuen/PlaySharedTypeScriptClasses


Happy Coding!

No comments:

Post a Comment