How can I define an AngularJS service using a TypeScript class that doesn't pollute the global scope?
Asked Answered
J

4

37

I am using AngularJS and TypeScript. I want to implement an AngularJS service using a Typescript class, like this:

class HelloService {
    public getWelcomeMessage():String {
        return "Hello";
    }
}

angular.module('app.services.helloService', []).factory('helloService', () => {
    return new HelloService();
});

This compiles to the following javascript code:

var HelloService = (function () {
    function HelloService() {
    }
    HelloService.prototype.getWelcomeMessage = function () {
        return "Hello";
    };
    return HelloService;
})();

angular.module('app.services.helloService', []).factory('helloService', function () {
    return new HelloService();
});

This pollutes the global namespace with the variable HelloService, which I obviously don't want. (Using Chrome's console I verified that HelloService was an object.) How can I solve/avoid this problem?

I tried the obvious:

angular.module('app.services.helloService', []).factory('helloService', function () {
    class HelloService { ...} 
    return new HelloService();
});

but that gives me a compile error ("Unexpected token; 'statement' expected.").

One possible solution I can think of is using TypeScript's import and export somehow, which in turn will use RequireJS. This probably will wrap the HelloService within a define function, thus avoiding pollution of the global scope with HelloService. However, I don't want to use RequireJS in my AngularJS application for now, as I think AngularJS is good enough for my use, and it adds complexity.

So, my question is, how can I define an AngularJS service using a TypeScript class that doesn't pollute the global scope?

Juvenescent answered 18/10, 2013 at 10:53 Comment(0)
J
15

I should provide what I actually ended doing:

module MyModule {
    export class HelloService {
        public getWelcomeMessage():String {
            return "Hello";
        }
    }

    angular.module('app.services.helloService', []).factory('helloService', () => {
        return new HelloService();
    });
}

In this way I can use

return new HelloService();

instead of

return new MyModule.HelloService();
Juvenescent answered 7/3, 2014 at 9:1 Comment(3)
To simplify more, use .service('helloService', HelloService) instead of factory as that will call new on the constructor for you.Stupefaction
I would argue that you could even leave out the export statement on the class. The code as shown above doesn't need it since you're not accessing this class from outside the module. Maybe I'm being daft, just wrapping my head around using TypeScript w/Angular.Flatways
@SunilD. you would normally export an interface that is implemented by the service. This way you can have typechecks and codecompletion/intellisense etc when you inject the service. You could also use the exported class for that, but using an interface as a type is more clean I believe :)Coranto
C
40

2016-05-06: New example using ES6-style modules

The static $inject array and constructor remain unchanged from the previous example.

The only change is to split the classes into multiple files and use ES6 modules to pull in the class definitions.

/lib/HelloService.ts:

export class HelloService {
    public getWelcomeMessage():String {
        return "Hello from HelloService";
    }
}

/lib/AnotherService.ts:

import {HelloService} from './HelloService';

/**
 * Service that depends on HelloService.
 */
export class AnotherService {

    // Define `HelloService` as a dependency.
    static $inject = ['HelloService'];
    constructor(
        // Add the parameter and type definition.
        public HelloService: HelloService
    ){}

    public getWelcomeMessage():String {
        // Access the service as: `this.HelloService`
        // Enjoy auto-completion and type safety :)
        var helloMsg = this.HelloService.getWelcomeMessage();
        return "Welcome from AnotherService, " + helloMsg;
    }
}

/index.ts:

// Using the services.
import {HelloService} from './lib/HelloService';
import {AnotherService} from './lib/AnotherService';

angular.module('HelloApp', [])
    .service('HelloService', HelloService)
    .service('AnotherService', AnotherService)
    .run(['AnotherService', function(AnotherService: AnotherService){
        console.log(AnotherService.getWelcomeMessage());
    }]);

Previous answer: using namespaces

Building from Steve Fenton's answer:

To allow dependency injection, add a static $inject array on your class.

See the Angular $injector documentation on how the $inject array works.

The dependencies will be injected into your constructor in the order given by the array (and makes it work with minification).

Dependency Injection Example:

namespace MyModule {
    /**
     * Angular Service
     */
    export class HelloService {
        public getWelcomeMessage():String {
            return "Hello from HelloService";
        }
    }

    /**
     * Service that depends on HelloService.
     */
    export class AnotherService {

        // Define `HelloService` as a dependency.
        static $inject = ['HelloService'];
        constructor(
            // Add the parameter and type definition.
            public HelloService: MyModule.HelloService
        ){}

        public getWelcomeMessage():String {
            // Access the service as: `this.HelloService`
            // Enjoy auto-completion and type safety :)
            var helloMsg = this.HelloService.getWelcomeMessage();
            return "Welcome from AnotherService, " + helloMsg;
        }
    }
}


// Using the services.
angular.module('app.services.helloService', [])
    .service('HelloService', MyModule.HelloService)
    .service('AnotherService', MyModule.AnotherService)
    .run(['AnotherService', function(AnotherService: MyModule.AnotherService){
        console.log(AnotherService.getWelcomeMessage());
    }]);
Closegrained answered 28/8, 2014 at 3:31 Comment(1)
(yourSolution) => {comment = 'perfect, bright, simple, straight'; return 'cheers'; }Spermatogonium
J
15

I should provide what I actually ended doing:

module MyModule {
    export class HelloService {
        public getWelcomeMessage():String {
            return "Hello";
        }
    }

    angular.module('app.services.helloService', []).factory('helloService', () => {
        return new HelloService();
    });
}

In this way I can use

return new HelloService();

instead of

return new MyModule.HelloService();
Juvenescent answered 7/3, 2014 at 9:1 Comment(3)
To simplify more, use .service('helloService', HelloService) instead of factory as that will call new on the constructor for you.Stupefaction
I would argue that you could even leave out the export statement on the class. The code as shown above doesn't need it since you're not accessing this class from outside the module. Maybe I'm being daft, just wrapping my head around using TypeScript w/Angular.Flatways
@SunilD. you would normally export an interface that is implemented by the service. This way you can have typechecks and codecompletion/intellisense etc when you inject the service. You could also use the exported class for that, but using an interface as a type is more clean I believe :)Coranto
R
6

I have two solutions, the first gives you class-based syntax, the second leaves absolutely nothing in the global scope...

You could compromise slightly by only adding a single handle to the global scope (this really applies if you have multiple classes that you want to avoid placing in the global scope as currently you only have one class).

The following code leaves only the module in the global scope.

module MyModule {
    export class HelloService {
        public getWelcomeMessage():String {
            return "Hello";
        }
    }

    export class AnotherService {
        public getWelcomeMessage():String {
            return "Hello";
        }
    }

}

angular.module('app.services.helloService', []).factory('helloService', () => {
    return new MyModule.HelloService();
});

angular.module('app.services.anotherService', []).factory('anotherService', () => {
    return new MyModule.AnotherService();
});

Alternatively, to leave not a single thing in global scope, you could avoid the class syntax and use "plain old JavaScript":

angular.module('app.services.helloService', []).factory('helloService', () => {
    var HelloService = (function () {
        function HelloService() {
        }
        HelloService.prototype.getWelcomeMessage = function () {
            return "Hello";
        };
        return HelloService;
    })();

    return new HelloService();
});
Riorsson answered 18/10, 2013 at 13:21 Comment(4)
What about dependecy injections?Otic
See my answer below (https://mcmap.net/q/415869/-how-can-i-define-an-angularjs-service-using-a-typescript-class-that-doesn-39-t-pollute-the-global-scope) to see how dependency injection can be handled using this approach.Closegrained
Yngvar Kristiansen's answer with Nathan's comment is actually betterSuchta
Downvoted because you can of course use IIFEs in TypeScript, meaning you do not have to introduce any global names. Class syntax and namespace pollution are orthogonal. Also, if you are registering a class, please use the angular.service method. That is what it is there for. The factory that creates an instance of the class is completely unnecessary and leads to confusing code.Howlan
S
-1

This is the method that I'm following:

module HelperServices {
    export class helloService {
       sayHelloWorld: () => string;
        constructor() {
            this.sayHelloWorld = () => {
                return "Hello World";
            }
        }
    }
}

As simple as that..

Salamone answered 5/2, 2016 at 8:36 Comment(1)
But this does not work with dependency injection, your other modules will explicitly depend on that class, so you can't mock it easily for instance. See the angularjs documentation on why they chose to use dependency injection.Nativeborn

© 2022 - 2024 — McMap. All rights reserved.