The Practical Client

Managing Modules in TypeScript 2.1

If you want to ensure that the right code is loaded at the right time (and only loaded when you need it), you can use TypeScript code to organize your code into modules. As a side benefit, managing your script tags will get considerably easier.

When combined with a module manager, TypeScript modules are great for two reasons. First, modules eliminate the need for you to make sure that you stack your script tags in the right order. Instead, within each script file you specify which other files your code depends on and the module manager establishes the right dependency hierarchy -- in your page, all you need is a single script tag for the code file that kicks everything off.

Second, the module manager loads your code files asynchronously as they're needed. If, as a user works through your application, some code file is never needed, then it will never get hauled down to the browser. Even if your user does use all the code files, deferring loading script files until you need them can significantly improve the loading time for your pages.

I discussed modules in my second Practical TypeScript column in May 2013 (except, back then, the column was called Practical JavaScript and I was using TypeScript 0.8.3). Much has changed in TypeScript since then and now's a good time to revisit the topic because some of those changes are part of the latest version of the language, TypeScript 2.1.

There has been one major change since the first version of TypeScript: What were called "internal modules" in the earliest versions of TypeScript are now called namespaces. TypeScript namespaces act like .NET Framework namespaces and should be used to organize related components (classes, enums and so on) to help developers discover them. This article uses "modules" in the current sense of the term: as a way of managing and loading script resources on an as-needed basis.

Setting Up
For this column I'm using TypeScript 2.1 in Visual Studio 2015. I used NuGet to add RequireJS to my project as my module loader. You don't, however, have to use RequireJS. TypeScript syntax for working with modules is "implementation-neutral": Whatever code you write in TypeScript is translated by the compiler into whatever function calls are required by your module loader. To tell the TypeScript compiler to generate RequireJS-compatible JavaScript code, I added this line to my tsconfig.json file which specifies that I'm using the UMD format which is compatible with both AMD (the original module loader, implemented in RequireJS) and CommonJS (which is popular with Node developers):

"module": "umd" 

The code in this article works equally well with module set to amd. Angular developers using SystemJS should set module to system (I'm told).

Picking RequireJS does mandate the format of the script tag that I use to start my application. First, of course, I have to add a script tag to my page to load the RequireJS library. However, to cause RequireJS to load my application's initial script file, I must add a data-main attribute to the script tag that references that initial script file. The path to my script file is usually a relative filepath (relative to the location of the RequireJS script file).

For this case study, I put all my files in the Script folder. However, my RequireJS script file is directly in my application's Scripts folder, while my initial script file (CustomerManagement.ts) is in a subfolder called Application. To invoke RequireJS and point it at my initial script file, I add this tag to my page's <head> element:

<script src="Scripts/require.js" 
  data-main="/Application/CustomerManagement.js"></script>

Notice that, no matter how many files I organize my TypeScript code into, I only need a single script tag on each page. When RequireJS loads CustomerManagement.js, it will check to see what files CustomerManagement.js requires and load them (and so on, through the dependency hierarchy).

Creating an Export File
To create a file of useful code that I'd like to be available for use in some other file, all I need to do is create a TypeScript file and add my declarations to it. I add the keyword export to each declaration that I want to use outside the file. Listing 1 shows an example of a CustomerEntities file that exports an enum, a constant, an interface, a base class, a derived class and a function. To make this case study more interesting I put this file in another Scripts subfolder, which I called Utilities.

Listing 1: The CustomerEntities Module
export enum CreditStatusTypes {
  Excellent,
  Good,
  Unacceptable
}
export const defaultCreditStatus: CreditStatusTypes = CreditStatusTypes.Good;

export interface ICustomer {
  Id: string;
  FirstName: string;
  LastName: string;
}

export class Customer implements ICustomer {
  Id: string;
  public FirstName: string;
  public LastName: string;
}

export class PremiumCustomer extends Customer {
  public CreditLimit: number;
  public CreditStatus: CreditStatusTypes;
}

function MakeNewPremiumCustomer(custId: string): PremiumCustomer {
  let pcust: PremiumCustomer;
  pcust = new PremiumCustomer();
  pcust.Id = custId;
  pcust.CreditStatus = defaultCreditStatus;
  pcust.CreditLimit = 100000;
  return pcust;
}

I could have enclosed my exported components in a module, like this:

export module Customers
{
  export enum CreditStatusTypes {
  //...rest of module...

However, that module name establishes a qualifier that I'd have to use when referring to any component in the module. For example, in any code that uses CreditStatusTypes enum I'd have to refer to it as Customers.CreditStatusType. That may not seem like a bad thing -- it might even sound helpful because it could help avoid name collisions if you import two modules, both of which contain something called CreditStatusType.

But, as I'll discuss in a later column, you have other options to handle name collisions. As a result, using a module may simply insert another namespace level into your component's names without adding any value. Nor does enclosing all the components in the module save you any code -- you still have to flag each component with the export keyword (as I did with the CreditStatusTypes in my example). I don't use the module keyword in my modules.

Importing Components
Now, in my application code, to create a PremiumCustomer class, I need access to the MakeNewPremiumCustomer function and the PremiumCustomerClass. To get that access, I just add an import statement that imports those components from my module. Because my application code is in a file in my Scripts/Application folder, the import statement it uses (with a relative address pointing to my CustomerEntities module in Scripts/Utilities), looks like this:

import { PremiumCustomer, MakeNewPremiumCustomer } from "../Utilities/CustomerEntities"

By the way, if you've set noUnusedLocal to true in your tsconfig file, then you'll be obliged to use any component you reference in your import statements or your code won't compile.

But I'm not done yet. While I've reduced the mass of script tags that I might need at the start of a page to a single reference to RequireJS, I've only looked at the simplest ways to export and import module components. Next month I'm going to focus on some more architectural issues you should consider when creating modules (along with the code you need to manage those modules, of course).

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