Skip to content

Latest commit

 

History

History
374 lines (269 loc) · 9.3 KB

File metadata and controls

374 lines (269 loc) · 9.3 KB

Typescript Guidelines



File's name should be ServiceName.service.ts

module ModuleName {

    // Export service class, a class should always start with capital letter
    export class ServiceName {

        // variables before constructor
        private _privateVariable: string = 'foo';

        // always use the 'private' keyword when injecting
        constructor (
            // angular specific elements ($) injected first
            private $http: ng.IHttpService,
            // our items injected after
            private someTripService: TR.ISomeTripService
        ) {}

        // Always use generics when returning a promise value
        exposedFunction (foo: string, bar: number[]): ng.IPromise<Models.TypeReturned> {
            this.$http.get('url');
        }

        // use the underscore notation for private items
        // put private functions at the end of the file
        private _privateFunction: number (foo: number) {
            return foo++;
        }
    }

    angular
        .module('moduleName')
        .service('serviceName', ServiceName);
}


File's name should be ControllerName.controller.ts

module ModuleName {

    // most of the time there is no need to export the controller class
    class ControllerName {

        private _privateVariable: string = 'foo';

        // exposed on scope
        someValue: string = 'bar';

        constructor (
            // use `ng` keyword when injecting angular elements
            private $rootScope: ng.IRootScopeService
            private someTripService: TR.ISomeTripService
        ) {}

        // pass all the external parameters to the function via template, if
        // possible avoid using closures
        exposedOnScope (foo: number, bar: string) : void {
            this.someTripService.someExposedFunction(foo, bar);
        }

        // a function should always return a type
        private _someCalculation (firstVal: number, secondVal: number) : void {
            console.log(firstVal + secondVal);
        }

        // Use the `angular` keyword when creating controllers/services/...
        angular
            .module('moduleName')
            .controller('controllerName', ControllerName);
    }

}


File's name should be DirectiveName.directive.ts While controllers and services are instance of a class a directive expects a function returning an object, in order to adopt the same 'class approach' we have on all the other elements we will make use of the bindToController property which let us bound properties to a controller instead of the directive scope (full explanation)

module ModuleName {

    class DirectiveName {

        private _privateVariable: string = 'foo';

        // the property is binded to this variable,
        // it needs to be explicitly declared
        propertyFromOutside: string;

        // exposed on scope
        someValue: string = 'bar';

        constructor (
            private $rootScope: ng.IRootScopeService
            private someTripService: TR.ISomeTripService
        ) {}

        exposedOnScope () : void {
            console.log(this.propertyFromOutside);
        }
    }

    angular
        .module('moduleName')
        .directive('directiveName', (): ng.IDirective => {
            return {
                scope: {},
                bindToController: {
                    propertyFromOutside: '='
                },
                controller: DirectiveName,
                controllerAs: 'dn',
                link: function ($scope, $el, $attrs, ctrl) {
                    /*
                        any DOM interaction with the directive element is described
                        here - you have access to all the exposed functions of the
                        controller, avoid using this function for any other reason
                    */
                    $el.on('click', ctrl.exposedOnScope);
                }
            }
        });
}


In Typescript, most of the time, classes and interfaces are interchangeable (a class can be used as a type exactly like an interface). Usually the developer should decide on a case by case basis. One tip though: if we just need the shape of the object (i.e. we get it only as a promise result) we can prefer an interface, on the other side if we expect to need an instance of that type of object we can just pick a class.

repetita iuvant - bear in mind that this is not an absolute rule - just a suggestion.



DO NOT USE VAR

var hoists the variable at the top of the enclosing function block, this behaviour can be replicated with a good positioning of let.



Every module should be self-contained in an internal typescript module:

    module ModuleName {
        export class Something {

        }
    }

Do not use submodules unless strictly necessary.



Do not use coding structures inside your names:

    module ModuleName {
        // Nono
        export class FunctionalityController {

        }

        // Yup!
        export class Functionality {

        }
    }

Remember that a name should be unique only inside the containing module:

    module ModuleOne {
        export class Foo {

        }
    }

    module ModuleTwo {
        export class Foo {

        }
    }

    // This is perfectly fine as the full name is descriptive enough.
    module Module {
        class Bar {
            let varOne: ModuleOne.Foo,
                varTwo: ModuleTwo.Foo
        }
    }


From the Typescript Handbook:

The 'any' type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type-checking during compilation.

While the use of any will be allowed by the compiler (for the moment), you should probably not use it. The whole point of moving to Typescript is improving the tooling and the code quality via a static type language, using any defeats this purpose.

So, if you find yourself thinking that any is absolutely needed in the piece of code you're writing, double check if there is any (ahah) other way to do it. Then triple check.



The Typescript compiler will produce an error with this syntax

    class Controller {
        constructor (
            private $scope: ng.IScope
        )

        foo () {
            this.$scope.bar = 'foo';
        }
    }

Because there is no variable bar defined on the ng.IScope interface. In order to avoid this problem there are two solutions:

Using bracket notations:

    class Controller {
        constructor (
            private $scope: ng.IScope
        )

        foo () {
            this.$scope['bar'] = 'foo';
        }
    }

Extending the ng.IScope interface

    interface IMyScope extends ng.IScope {
        bar: string
    }

    class Controller {
        constructor (
            private $scope: IMyScope
        )

        foo () {
            this.$scope.bar = 'foo';
        }
    }


Let's consider this:

// Replaces string vars by argument index+1
// i.e. {0} is replaced by second argument
String.format = function(tokenised){
    let args = arguments;
    return tokenised.replace(/{[0-9]}/g, function(matched){
        matched = matched.replace(/[{}]/g, '');
        return args[parseInt(matched)+1];
    });
};

Here we are exteding the basic String object by adding the function .format(tokenised), while this in javascript is perfectly valid in a typescript file this code:

let world: string = 'World';
let foo = String.format('Hello {0}!', world);

would produce a compiler error, because the type StringConstructor isn't exposing any function called format(tokenised).

This is solved by using declaration merging in a proper declaration file:

interface StringConstructor {
    format(str : string, ...tokens : any[]) : string;
}


  • Use a 100 columns limit, files are more readable and it's easier to work with two files side by side
  • 4 spaces indentation