The Practical Client

Structuring and Loading TypeScript Modules

TypeScript provides native support both for organizing your application's client-side code into a set of modules, and for freeing you from having to manage the resulting script tags.

As I've shown in previous columns, it's easy to modularize a TypeScript application into multiple files that reference each other. However, that creates a problem around managing the script tags that download the resulting JavaScript code. You need to ensure all your modules are downloaded, and in the right order. I could avoid this problem by putting all my code in a single file, but that would create a nightmare when it comes to source control, maintaining the application or reusing the modules.

Fortunately, this is a problem that's easier to solve in TypeScript than in JavaScript because, as a compiled language, TypeScript can work out all dependencies between modules at compile time. As a side benefit, TypeScript can also reduce the time it takes to start up your application by downloading your script files asynchronously, rather than waiting for the browser to download each code file one at a time.

Modularizing the Application
There are several ways to categorize your TypeScript code. One way of categorizing files is based on when the files are used. While all your TypeScript files will generate JavaScript files, not all of those JavaScript files will be used at runtime.

For example, I keep all interface definitions for my business entities in separate files because I can use those interfaces in any project that needs those entities. Keeping the interfaces in separate files lets me reuse them. The contents of a typical interface file (called Interfaces.ts) look like this:

module PHVEntities
{
  export interface ICustomer extends breeze.Entity 
  {
    Id: number;
    FirstName: string;
    LastName: string;
    SalesOrders: ISalesOrder[];
  }
}

In another file I have the client-side code that drives my application. It needs to reference my interfaces in order to compile. To use the interfaces, I add an import statement that references the module containing the interfaces. The import statement also defines a reference for that module that I can use in my application.

As an example, the code in Listing 1 (in a file called ViewModel.ts) has two import statements: one for my PHVEntities module, and one for the Breeze library I use to call my server-side services. The code then uses both of those references in a method called fetchAllCustomers.

Listing 1: Method fetchAllCustomers
import ent = PHVEntities;
import b = breeze;

export module PhvVM
{
  export class SalesOrderVM
  {
    // ...omitted code...
    fetchAllCustomers() 
    {
      b.EntityQuery
       .from("Customers")
       .using(this.em)
       .execute()
       .then(dt => {
                    this.customers(<ent.ICustomer[]> dt.results);
                })
       .fail(err => { "Problem:" + alert(err.message); });
     } 

I've used the export keyword in the Entities.ts file so that I can use the SalesOrderVM class (and its methods) from another file.

To download the code in the ViewModel.ts, I add a script tag to my browser for the resulting JavaScript file:

<script src="Scripts/Application/ViewModel.js"></script>

The import statements allow TypeScript to work out the dependencies among my modules. It doesn't, however, have any effect on how the code is downloaded by the browser. The import statement I've used in the ViewModel file, for example, won't cause the code in the Entities file to be brought down to the browser -- but the code in the Entities file only defines types and, while required at compile time, it isn't needed at runtime. If the Entities file isn't downloaded to the browser, it doesn't matter.

Managing Downloading
I can use a different form of the import statement to not only establish dependencies between my files, but also to trigger downloading the files. Instead of referencing a module in the import statement, I use the TypeScript require method, passing the name of the file holding the code. At compile time, the compiler will find the file and use its code to support the compile process; at runtime, whatever download method you choose to use will find the file and download it.

As an example, the import statement in the following code (in a file called UIIntegration.ts) references the ViewModel.ts file containing the SalesOrderVM class:

import vm = require("PhvVM");
$(function () {
    var so: vm.PhvVM.SalesOrderVM;
    so = new vm.PhvVM.SalesOrderVM();
    so.fetchAllCustomers();
  });

As with the previous version of the import method, the statement also establishes a reference (called vm) to the contents of the file. The code uses that vm reference to instantiate the class in the file and call one of the class methods.

With this format of the import statement, the TypeScript compiler will generate the appropriate JavaScript code to trigger downloading the file using either CommonJS or RequireJS (I'll discuss how to use RequireJS shortly).

You can't seamlessly switch between the two versions of the import statement (at least in TypeScript 0.9.5). I can use the version of the import statement without the require method to reference code, like this enumerated value, that I've defined inside a module:

module PHVEnumValues {
  export enum CustomersType {
    Deadbeat,
    Standard,
    Premium
  }
}

On the other hand, if I want to use the import statement to trigger downloading the file (and I'm using the import statement with the require method) , I have to also declare the module with the export keyword:

export module PHVEnumValues {
  export enum CustomersType {
    Deadbeat,
    Standard,
    Premium
  }
}

If I declare my enumerated value outside of a module, as in the following example, I also have to use the version of the import statement with the require method just to get a reference I can use at compile time:

export enum CustomersType {
    Deadbeat,
    Standard,
    Premium
  }

Using RequireJS
Using RequireJS with TypeScript requires two steps. First, you need to use the NuGet Manager to download and install the RequireJS JavaScript library. Then you need to tell the TypeScript compiler to use RequireJS for your project. To configure the compiler in Visual Studio 2013, go to Project Properties | TypeScript Build | Module System and select the AMD option (or CommonJS if you're not using AMD). If you're using an earlier version of Visual Studio, first install Mads Kristensen's Web Essentials through the Tools | Extensions menu choice. Then, in Tools | Options | Web Essentials | TypeScript, set the Use AMD Module option to True. This difference between versions of Visual Studio means that, while Visual Studio 2013 lets you set this option on a project-by-project basis, in earlier versions of Visual Studio you must set the option for all of your projects.

If you're using RequireJS, you need a script tag in your Web page that downloads the require.js file. The tag must also specify, in its data-main attribute, the initial file for your application. Any import statements in that file that use the require method will trigger downloading additional files your application requires -- no additional script tags are required.

This example downloads the UIIntegration file containing the import statement that triggers downloading the ViewModel file:

<script data-main="Scripts/Application/UIIntegration.js" 
  type="text/javascript" 
  src="Scripts/require.js"></script>

As you can see, TypeScript allows you to have each file be responsible for specifying the files or modules on which it depends. TypeScript also allows you to control which of those files will be downloaded to the browser. The TypeScript syntax generates the JavaScript code for two tools for managing downloads (and is intended to be compatible with whatever ECMAScript 6 ends up specifying in this area). More important, by letting you modularize your application, TypeScript lets you assemble your applications out of a toolkit of well-defined modules.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube