Practical TypeScript

The Focus Is on Architecting 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 start organizing your TypeScript code into focused files right now.

In last month's column, I introduced the fundamentals of defining, exporting and importing modules in TypeScript 2.1. Those tools allow you to organize your code into meaningful modules that you can then mix-and-match to create new applications…but I didn't take much advantage of that. Instead, I had a single file containing a hodge-podge of components: both entity-related definitions (classes, enums, constants) and helper functions for manipulating those classes.

It's not hard to imagine that some other application might want to use the entity classes but with a different set of helper functions. Unfortunately, with everything in one big module, that's not possible. Listing 1 and Listing 2 show a better design with two files/modules: One module (CustomerEntities) contains the entity classes and the other module (PremiumCustomerFunctions) contains the helper functions that use those entities. Another application could use the entity class module without having to drag along the functions.

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;
}

Listing 2: The PremiumCustomerFunctions Module

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

Chaining Modules
Moving my MakeNewPremiumCustomer function to the new PremiumCustomerFunctions module creates a problem: My function won't work without the PremiumCustomer and CreditStatusTypes in the original file. Fortunately, the solution is easy: My new PremiumCustomerFunctions module just needs to import the CustomerEntities module. The import statement in PremiumCustomerFunctions that would make the PremiumCustomer and CreditStatusType available in PremiumCustomerFunctions would look like this:

import { PremiumCustomer, CreditStatusTypes } from "./CustomerEntities"
   
export function MakeNewPremiumCustomer(custId: string): PremiumCustomer {
  let pcust: PremiumCustomer;
  // ...Rest of function...

Of course, my MakeNewPremiumCustomer function isn't much use to my application code unless my application also has access to the PremiumCustomer class. I could solve this problem by having my application code import both the PremiumCustomerFunctions and CustomerEntities modules, like this:

import { PremiumCustomer } from "../Utilities/CustomerEntities"
import { PremiumCustomerFactory } from "../Utilities/PremiumCustomerFunctions"

And there would be nothing wrong with that. There is another solution, though: I can also add an export statement to my PremiumCustomerFunctions file to re-export the PremiumCustomer class. That export statement would look like this:

export { PremiumCustomer }

Now, my application file would only have to import the PremiumCustomerFunctions module to get both the MakeNewPremiumCustomer function and the PremiumCustomer class. For the record, that single import statement in my application code file would look something like this:

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

If an application does import from both CustomerEntities and PremiumsCustomerFunctions, the developer will need to make sure that he only imports PremiumCustomer from one of the modules.

Importing All
Of course, as I add new functions to my PremiumCustomerFunctions file, I'll probably end up importing more components from CustomerEntities (if you've been looking closely at the code you may have noticed that my PremiumCustomerFunctions doesn't have access to the defaultCreditStatus constant that's used in the MakeNewPremiumCustomer function). Adding all the individual components in CustomerEntities to my import statement would get very tedious, very quickly (even if you're paid by the hour).

You can, instead, use a wildcard in the import statement to grab every exportable item in a module. However, if you do that, you must also, effectively, assign a namespace to the imported components. An import statement in PremiumCustomerFunctions that uses a wildcard to import all of the items in CustomerEntities might look like this:

import * as Customers from "./CustomerEntities"

The "as Customers" establishes a qualifier that must be used with the names of the components imported from CustomerEntities. This namespace in the import statement helps avoid name collisions when importing from multiple modules. But, because I'm using the wildcard with the required qualifier, I'll need to rewrite my MakeNewPremiumCustomer function to use that qualifier when referring to the imported components. The resulting code looks like this:

function MakeNewPremiumCustomer(custId: string): Customers.PremiumCustomer {
  let pcust: Customers.PremiumCustomer;
  pcust = new Customers.PremiumCustomer();
  // ...Rest of function...

Assigning Aliases and Avoiding Name Collisions
While the qualifier required when using the wildcard helps avoid name collisions when importing all the components, even if I was importing a single component, I could run into a name collision. To avoid that, I can assign an alias to any component I import when I import it. This import statement, for example, assigns the alias PremCust to the PremiumCustomer class (the following code uses the alias):

import { PremiumCustomer as PremCust, 
  CreditStatusTypes, defaultCreditStatus } from "./CustomerEntities"
   
export function MakeNewPremCust(custId: string): PremCust {
  let pcust: PremCust;
  pcust = new PremCust();
  // ...Rest of the function...

You can also assign aliases in your export statement if you want to make a component available with a "better" name than it was declared with. I could use this code in the PremiumCustomerFunctions module to supply a name for the MakeNewPremiumCustomer that is hipper and more "with it" than its existing name:

export { MakeNewPremiumCustomer as PremiumCustomerFactory }

You can also use this feature to separate the act of exporting a component from the component's definition. As an example, this code defines my MakeNewPremiumCustomer function without using the export keyword. The module still exports the function, however, but does it with a separate export statement (this time without renaming the function):

function MakeNewPremiumCustomer(custId: string): PremCust {
  // ...Rest of function...
} 
export { MakeNewPremiumCustomer }

Default Imports
Of course, to import from a module, you must either use a wildcard or know the names of the components you want to import (Visual Studio and IntelliSense will help you out here). However, in any module, you can define one default export by adding the default keyword to its declaration. This example makes MakeNewPremiumCustomer the default export for its module:

export default function MakeNewPremiumCustomer(custId: string): PremCust {
  let pcust: PremCust;
  // ...Rest of function...

To import the default export, you use any name you want in the import statement and omit the curly braces. This example would import the default function under the name PCustFactory:

import PCustFactory from "../Utilities/PremiumCustomerFunctions"

From an architectural point of view, default exports work especially well if it provides access to the rest of the components (much like the jQuery prototype provides access to all of the jQuery functionality).

Importing the default export can be combined with selectively importing other components in the same module. This code, for example, imports the default component along with PremiumCustomer:

import PCustFactory, { PremiumCustomer } from "../Utilities/PremiumCustomerFunctions"

There's more you can do, especially in the tsconfig file, to manage paths to your modules and, as a result, avoid using relative references in your import statements. However, with the tools you have here you can organize your code into meaningful, reusable packages (and, just as a bonus, simplify your script tags while speeding up your application by deferring script loading).

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
Upcoming Events

.NET Insight

Sign up for our newsletter.

I agree to this site's Privacy Policy.