left-icon

TypeScript Succinctly®
by Steve Fenton

Previous
Chapter

of
A
A
A

CHAPTER 5

Creating New Modules

Creating New Modules


When writing a new TypeScript program, you should start by designing the high-level modules. Each class and interface you add should be placed inside an appropriately named module, and if you don’t already have a module that they naturally fit into, you should consider adding a new one. If you are adding TypeScript to an application that has existing JavaScript files, there is more information on integrating the two languages in Chapter 6, "Working with existing JavaScript."

In this chapter, I will use a practical and usable example to demonstrate the features of the TypeScript language and associated tools. I will create a utilities module to house cross-cutting concerns, such as the logging example from the previous chapter.

Modules

Declaring a Module

Modules are declared with the module keyword. A module is a container to group together a set of related interfaces and classes. The interfaces and classes can be internal only, or made available to code outside of the module. Everything inside of a module is scoped to that module, which is preferable to adding items to the global scope.

If you are writing a large-scale application, you can nest your modules just like you nest namespaces in .NET. The aim should be to make the components of your program easy to discover using autocompletion. In this chapter I will name the program Succinctly and the first module Utilities.

Utilities Module

module Succinctly {

    module Utilities {

    }

}

Typically you would write your program using one module per file, so the Utilities module would live inside of Utilities.ts and be compiled into Utilities.js. It is possible to add more than one module to the same file, but it may make your program more difficult to comprehend. Additionally, you can declare a module across multiple files, which is useful for creating a namespace, or for extending a third-party library.

Note: Module declarations in TypeScript can vary depending on your module loading strategy. You can read more about how this affects your program in Chapter 5, "Loading Modules."

Adding a Class to a Module

To add a class to a module, you use the class keyword. You can then place variables and functions inside the class.

Logger Class (Utilities.ts)

module Utilities {

    export class Logger {

        log(message: string): void {

            if (typeof window.console !== 'undefined') {

                window.console.log(message);

            }

        }

    }

}

The interesting part of this example is the export keyword. This makes the Logger class visible outside of the Utilities module, which means you can create new instances of the Logger elsewhere in your code. If you omit the export keyword, you will only be able to use the Logger from other classes in the Utilities module.

The log function is public by default, but I’ll go into more detail about functions later in this chapter.

Assuming you have placed this module inside a file named Utilities.ts, you can now make use of the Logger class elsewhere in your program by referencing the module file in a special TypeScript reference comment. The reference is removed from the compiled JavaScript and simply helps the development tools understand what modules and classes are available for autocompletion and type checking. You can use multiple reference comments, but should try to make each module depend on as few other modules as possible.

Calling the Logger

///<reference path="Utilities.ts" />

window.onload = function () {

    var logger = new Utilities.Logger();

    logger.log('Logger is loaded');

};

You create a new instance of a Logger using the new keyword. The Logger must be referenced via its module. This gives you powerful code organization tools that closely match those available in .NETyou can imagine what the entire TypeScript program might look like:

  • Utilities.Logger
  • Utilities.Dates
  • Messaging.Ajax
  • Messaging.JsonConverter
  • Messaging.XmlConverter

To test this simple example, you can run it inside a browser using an HTML file. I have manually added the two scripts to the end of the body tag, but I will introduce some alternative techniques in Chapter 5, "Loading Modules."

HTML Page

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="utf-8" />

    <title>TypeScript Example</title>

</head>

<body>

    <h1>TypeScript Example</h1>

    <script src="Utilities.js"></script>

    <script src="Program.js"></script>

</body>

</html>

If you open the browser’s console, you should see the message Logger is loaded. To open the console, use the appropriate option for your browser:

Browser Console Access

Browser

Tools Access

Internet Explorer

Press F12 to open the developer tools. The console can be found under the Scripts tab.

Firefox

Press Ctrl+Shift+K to open the standard Web Console. There are also excellent productivity extensions for Firefox, such as the Firebug add-in, which also hosts the console.

Opera

Press Ctrl+Shift+I to open Dragonfly, Opera’s built-in developer tools. The console is one of the main tabs.

Safari

Press Ctrl+Alt+I to open the developer tools.

Chrome

Press Ctrl+Shift+J to open the developer tools and jump straight to the console.

Interfaces, Classes, and Functions

Private Functions

In some console windows, the date and time of the message is automatically displayed, but not in all cases. You could extend the logger to show the time automatically next to the message.

Private Functions

module Utilities {

    export class Logger {

        log(message: string): void {

            if (typeof window.console !== 'undefined') {

                window.console.log(this.getTimeStamp() + ' - ' + message);

            }

        }

        private getTimeStamp(): string {

            var now = new Date();

            return now.getHours() + ':' +

                now.getMinutes() + ':' +

                now.getSeconds() + ':' +

                now.getMilliseconds();

        }

    }

}

I have added a getTimeStamp function using the private keyword. The private keyword tells that this function is not available outside of the Logger class. If you attempt to call the method outside of this class, you will get a warning.

Attempting to call Private Functions

///<reference path="Utilities.ts" />

window.onload = function () {

    var logger = new Utilities.Logger();

    logger.getTimeStamp();

};

The private keyword can be used on functions and variables, but it is important to understand that this only protects them at design time. Once your TypeScript code has been compiled into JavaScript, there is nothing to stop them from being used, so you cannot rely on them if you are making your libraries available to other developers.

Note: Type safety, interfaces, and protection of private variables and functions only occurs at design time and compilation time in TypeScript.

Static Functions

If you run the latest example, you will see that the message is now prefixed with a timestamp, but that the format isn’t quite right: 15:39:1:767 - Logger is loaded. When the hours, minutes, or seconds are less than two digits, or when the milliseconds are less than three digits, the value should be left-padded with zeros to preserve the time stamp format.

Static Functions

module Utilities {

    export class Logger {

        log(message: string): void {

            if (typeof window.console !== 'undefined') {

                window.console.log(this.getTimeStamp() +

                    ' - ' + message);

            }

        }

        private getTimeStamp(): string {

            var now = new Date();

            return Formatter.pad(now.getHours(), 2, '0') + ':' +

                Formatter.pad(now.getMinutes, 2, '0') + ':' +

                Formatter.pad(now.getSeconds(), 2, '0') + ':' +

                Formatter.pad(now.getMilliseconds(), 3, '0');

        }

    }

    class Formatter {

        static pad(num: number, len: number, char: string): string {

            var output = num.toString();

            while (output.length < len) {

                output = char + output;

            }

            return output;

        }

    }

}

I have added a Formatter class to the Utilities module that contains the function that will format the number by pre-pending zeros and returning the resulting string. The first point to note is that I have omitted the export keyword, which means that the Formatter class is only available inside of the Utilities module. It is not accessible anywhere else in the program.

I have done this deliberately in this case, as I don’t want to expose this class outside of the Utilities module, but if I change my mind in the future, I can simply add the export keyword to expose the class. If something isn’t appearing in your autocompletion list when you think it should, you are probably missing the export keyword.

The second new keyword I have used in this example is the static keyword. This keyword allows me to call the pad function without creating an instance of the Formatter class, which you would normally do like this: var formatter = new Formatter();.

This is now a working module that adds a neatly formatted time stamp to any message being logged. It is now possible to take a step back and re-factor the example to make it cleaner and easier to use. TypeScript has language features that you can use to clean up your use of functions, and I’ll introduce them next.

The following code samples are all based on the lLogging example, but I have just included specific sections of code to shorten the examples and to avoid boring you with repetition. You can continue to follow along using the code that was created during this chapter; just slot in the updated sections as you go.

Default Parameters

By specifying a default value for a parameter, calling code can treat the parameter as optional. You may already be familiar with default parameters, which were added to C# in .NET version 4 and offer a clean alternative to overloaded methods in many cases.

To specify a default parameter, you simply add it to your function using an equal sign after the type declaration. I have added defaults to the pad function to specify the most common len and char input values.

Default Parameters

// before
static pad(num: number, len: number, char: string) {

// after
static pad(num: number, len: number = 2, char: string = '0') {

You no longer need to pass these arguments to the pad function, unless you want to use a value that is different from the default, so you can update your calling code to make it much cleaner and less repetitive. Specifically, you need to pass the num argument in all cases, because it isn’t sensible to specify a default value for this parameter. You only need to pass the len argument in the case of milliseconds, which should be three characters long, and you don’t need to pass the char argument at all.

Calling a function that has default parameters

// before
return Formatter.pad(now.getHours(), 2, '0') + ':' +

    Formatter.pad(now.getMinutes, 2, '0') + ':' +

    Formatter.pad(now.getSeconds(), 2, '0') + ':' +

    Formatter.pad(now.getMilliseconds(), 3, '0');

// after
return Formatter.pad(now.getHours()) + ':' +

    Formatter.pad(now.getMinutes()) + ':' +

    Formatter.pad(now.getSeconds()) + ':' +

    Formatter.pad(now.getMilliseconds(), 3);

Optional Parameters

Optional parameters are similar to default parameters, but are useful when you don’t actually need an argument to be passed to carry out the operation. You mark a parameter as optional by appending a question mark to its name.

Optional Parameters

static pad(num: number, len: number = 2, char?: string) {

    if (!char) {

        char = '0';

    }

You can check for the presence of the char argument using a simple if block. In this example, I set a value if one hasn’t been passed, which makes it behave just like a default parameterbut you could implement different logic flows based on whether optional parameters have been passed as arguments by calling code.

Rest Parameters

Rest parameters are placeholders for multiple arguments of the same type. To declare a rest parameter, you prefix the name with three periods. When you call a function that has a rest parameter, you are not limited to the number of arguments you pass, but they must be of the correct type.

Rest Parameters

function addManyNumbers(...numbers: number[]) {

    var sum = 0;

    for (var i = 0; i < numbers.length; i++) {

        sum += numbers[i];

     }

    return sum;

}

var result = addManyNumbers(1,2,3,5,6,7,8,9); // 41

The ...numbers parameter is declared as a rest parameter, so when we call addManyNumbers you can pass as many arguments as you need to. Any non-rest parameters should come before the rest parameter.

Mixed Normal and Rest Parameters

function addManyNumbers(name: string, ...numbers: number[]) {

Function Overloads

If you can solve your function definition requirements using default parameters and optional parameters, I recommend that you use these instead of function overloads. The function overload syntax in TypeScript works very differently from method overloads in .NET. Rather than specifying multiple methods with different signatures, TypeScript allows a single function to be decorated with multiple signatures.

To illustrate one potential use for function overloads, imagine that the pad function could be called either with a string to pad, or a number to pad.

Function Overloads

static pad(num: number, len?: number, char?: string);

static pad(num: string, len?: number, char?: string);

static pad(num: any, len: number = 2, char: string = '0') {

    var output = num.toString();

    while (output.length < len) {

        output = char + output;

    }

    return output;

}

I have added two function overload signatures above the original function. The first specifies that num is a number, and the second specifies that num is a string. Both the len and char parameters are marked as optional to allow calling code to omit them, in which case the default values specified in the original function declaration will apply.

The original pad function has been updated to specify a num parameter using the any type. This could be either a string or a number because of the two overloads, so I have used the any type to accept either of these, making num a dynamic parameter.

The function body remains untouched, as it was already converting the num parameter into a string. It just means that this call is redundant when the supplied argument is already a string. The actual function cannot be called when it is decorated with function overloads, so it is not possible to pass a value that isn’t a number or a string, even though the original function now accepts the dynamic any type, so it is more restricted and predictable to use function overloads than it is to simply change a parameter type to any.

I personally find this code block harder to read than two separate methods—for example padString and padNumber—so you may want to consider multiple functions as an alternative solution.

Alternative to Function Overloads

static padNumber(num: number, len?: number, char?: string) {

    return padString(num.toString(), len, char);

}

static padString(input: string, len: number = 2, char: string = '0') {

    var output = input;

    while (output.length < len) {

        output = char + output;

    }

    return output;

}

Constructors

Constructors in TypeScript are similar to .NET constructors and allow you to specify the dependencies and data that your class needs in order to work. Because you can’t create a new instance of the class without these things, you can guarantee they will be available in any non-static functions.

I will update the Logger class to remove its reliance on the static Formatter class and instead require an instance of a Formatter to be passed into the constructor.

Constructor

export class Logger{

    constructor (private formatter: Formatter) {

    }

    log(message: string): void {

        if (typeof window.console !== 'undefined') {

            window.console.log(this.getTimeStamp() +

                ' - ' + message);

        }

    }

    private getTimeStamp(): string {

        var now = new Date();

        return this.formatter.pad(now.getHours()) + ':' +

            this.formatter.pad(now.getMinutes()) + ':' +

            this.formatter.pad(now.getSeconds()) + ':' +

            this.formatter.pad(now.getMilliseconds(), 3);

    }

}

In the constructor, you can use either the public or private keyword to initialize the parameters and make them visible externally or internally, respectively. Unlike .NET constructors, you do not need to manually map the argument passed in the constructor onto a separate field as the compiler does this for you. You can then access the class-level variable using the this keyword.

To create an instance of the Logger class, you are required to pass in a Formatter. The development tools and compiler will check that you do.

Calling a Constructor

var formatter = new Utilities.Formatter();

var logger = new Utilities.Logger(formatter);

Remember, you’ll need to add the export keyword to the formatter.

Interfaces

The logging example created throughout this chapter is now in a pretty good state. The next stage is to create an interface for the Logger so that different implementations can be supplied, such as one that raises messages to the user if there is no console attached, or one that sends errors back to the server to be logged.

Interfaces are declared using the interface keyword and you can make an interface available outside of a module using the export keyword, just like you can with a class.

ILogger Interface

export interface ILogger {

    log(message: string): void;

}

To implement the interface on the Logger class, you use the implements keyword. TypeScript will now check that the class has the required variables and functions that are promised by the interface, and it will raise a warning if anything hasn’t been implemented, or if the implementation doesn’t correctly match the signature of the interface.

Implementing the ILogger Interface

export class Logger implements ILogger {

It is now possible to write new logging classes that also implement the ILogger interface safe in the knowledge that they can be used in place of each other wherever an ILogger is required. This will not affect the calling code because you can guarantee the concrete classes are compatible.

New Logger Class

export class AnnoyingLogger implements ILogger {

    log(message: string): void {

        alert(message);

    }

}

Multiple Interfaces

It is allowable in TypeScript for a class to implement more than one interface. To do this, separate each interface with a comma:

New Logger Class

export class MyClass implements IFirstInterface, ISecondInterface {

Duck Typing

An interesting feature of TypeScript interfaces is that they do not need to be explicitly implemented; if an object implements all of the required variables and functions, it can be used as if it did explicitly implement the interface. This is called duck typing, which you may have come across if you have used .NET’s iterator pattern.

I will use a new example to demonstrate this feature in isolation. Where an IPerson interface requires a firstName and lastName variable, you can create an anonymous object that TypeScript will accept as an IPerson type, even though it doesn’t claim to implement the interface. This also works if a class satisfies the requirements of the interface.

Duck Typing

interface IPerson {

    firstName: string;

    lastName: string;

}

class Person implements IPerson {

    constructor (public firstName: string, public lastName: string) {

   

    }

}

var personA: IPerson = new Person('Jane', 'Smith'); // explicit

var personB: IPerson = { firstName: 'Jo', lastName: 'Smith' }; // duck typing

Inheritance and Polymorphism

I will now return to the logging example in order to demonstrate inheritance and polymorphism. When I added the AnnoyingLogger class, it could already be used in place of the original Logger. This is the essence of polymorphism, whereby you can substitute any particular implementation of the logging class without affecting the calling code.

You can also achieve polymorphism using inheritance, which is useful if you want to alter only parts of the original class, or if you want to add additional behavior over and above what is offered by the original class. For instance, if you wanted to raise a visible message and write to the console, you could use inheritance, rather than call both loggers in turn.

Inheritance and Polymorphism

export class AnnoyingLogger extends Logger {

    log(message: string): void {

        alert(message);

        super.log(message);

    }

}

To inherit from a class, you use the extends keyword. The log function alerts the user before calling the base Logger class which you can access with the super keyword. The result of this function is that an alert dialog will be displayed, and when it is closed, a message will be added to the console, if one is running.

Calling the Logger

///<reference path='Utilities.ts'/>

window.onload = function () {

    var logger: Utilities.ILogger = new Utilities.AnnoyingLogger();

    logger.log('Logger is loaded');

};

Multiple Inheritance

A class can only inherit from one other class; it is not possible to inherit from multiple classes. This is the same restriction you will find in .NET and it avoids many pitfalls, such as confusion over the base class when super is called, and the famous Diamond of Death.

One interesting feature that TypeScript has that you won’t find in .NET is the ability for an interface to inherit from another interface. Just like class inheritance, you use the extends keyword to do this, but the purpose is slightly different. Rather than attempting to override or extend the behavior of a base class, you would use interface inheritance to compose interface groups. Unlike classes, interfaces can inherit from multiple interfaces.

Interface Inheritance

interface IMover {

    move() : void;

}

interface IShaker {

    shake() : void;

}

interface IMoverShaker extends IMover, IShaker {

}

class MoverShaker implements IMoverShaker {

    move() {

    }

    shake() {

    }

}

Because the IMoverShaker interface inherits from both IMover and IShaker, the class must implement both the move function and the shake function. This is a powerful feature that allows you to compose a number of interfaces into a super interface.

A Super Interface

The super interface in this diagram is the IAmphibian interface. A class that implements this super interface can do everything shown on the right-hand side of the diagram. Without the super interface, it would be hard to express that a class needed both IBoat and ICar interfaces to be valid for a given operation.

Using instanceof

The instanceof keyword can be used to compare an instance to a known type. It will return true if the instance is of the known type, or inherits from the known type. For example, the AnnoyingLogger class is an instance of AnnoyingLogger, and it is also an instance of Logger.

Instance Of

var isLogger = logger instanceof Utilities.Logger; // true

var isLogger = logger instanceof Utilities.AnnoyingLogger; // true

var isLogger = logger instanceof Utilities.Formatter; // false

Chapter Summary

I know that this has been an epic chapter, but you now know everything there is to know about TypeScript modules, interfaces, classes, and functions, including how to organize your code; set the visibility of classes, functions and variables; and how to use object-orientation in your TypeScript program design.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.