Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Organize code in TypeScript using modules

4 min read 1297

One of the things I really like about TypeScript is its ability to support modular programing architecture. After years of building JavaScript applications with rapt attention to design patterns and best practices, I’ve found that the ES6 module pattern is essential when considering structure and reusability of software.

In this article, we will discuss modules, what they are, and why and how to use them to improve the organization of your TypeScript code.

What are modules?

Modules are a type of design pattern in computer programming used to improve the readability, structure, and testability of code. They are supported by many programming languages including TypeScript. In simplified terms, modules are small pieces of code with independent program functionalities.

Why do we need modules?

A typical software grows gradually as new pieces of functionality are introduced. Structuring and preserving our codebase can be a tedious task initially, but doing the work upfront makes things easier the next time someone works on the codebase.

Without structuring our code, parts of our software can become deeply entangled, making it more difficult to reuse code and to look at any piece of code in isolation. Modules are an incredibly helpful tool for keeping code disentangled and accessible for reuse and testing.

Modules, as a organizational tool for code and its associated functions, allow us to:

  • Encapsulate our code.
  • Explicitly define the list of modules that the current one requires to function.
  • Structure our code in self-contained blocks
  • Test our code easily
  • Define public APIs
  • and more!

In the following sections, we will discuss how to use modules with TypeScript.

Exploring TypeScript modules

Now that we have a shared understanding of what modules are in software development, we can take a deeper dive into TypeScript modules and how to use them.

Given that TypeScript is a superset of JavaScript, it derives most of its concept from JavaScript including the JavaScript modules pattern introduced in ES6. It also uses the same export and import keywords as the JavaScript ES6 modules.

Using export syntax

In TypeScript, a piece of code remains internal to the module and cannot be accessed outside its module until exported. Once exported, the code becomes exposed.

We made a custom demo for .
No really. Click here to check it out.

Here is an example using export:

// module-1.ts 

//Private variable
let myApiKey: string = "Secret"; 

//Public variable
export const myPublicKey: string = "Public"; 

export enum MutationType {
  CreateTask = 'CREATE_TASK',
  SetTasks = 'SET_TASKS',
  RemoveTask = 'REMOVE_TASK',
  EditTask = 'EDIT_TASK',
}

// exported interface 
export interface Mutation{ 
  content: string; 
  type: MutationType; 
}

// private function
function logToConsole(mutation: Mutation): void {
    switch (mutation.type) {
        case MutationType.CreateTask:
            console.log(mutation.content);
    ...
        default:
            console.error(mutation.content);
    }
    console.log(message);
}

// exported function
export function log(mutation: Mutation): void {
    logToConsole(mutation);
}

Using import syntax

In TypeScript, an exposed piece of code in one module can be accessible outside its module using import.

Here is an example:

// my-module.ts
import { log, Mutation, MutationType, myPublicKey } from "./module-1";
console.log("Public key: ", myPublicKey);
const deleteTask: Mutation = {
    content: "Delete a task",
    type: MutationType.RemoveTask,
};
const createTask: Mutation = {
    content: "Create a task",
    type: MutationType.CreateTask,
};
log(deleteTask);
log(createTask);

Using TypeScript modules, we can import and export classes, type aliases, var, let, const, and other symbols.

Renaming with import

A very common concept in ES6 modules is renaming import. In TypeScript, it is possible to rename the exposed piece of code upon import using the following syntax:

// my-module.ts
import { publicKey as publicApiKey } from './module-1"

Alternatively, you can use the syntax below to import all of the contents of a module and give it a name of your choice:

// my-module.ts
import * as anyName from './module-1"

Finally, by setting the esModuleInterop option to true in tsconfig.json, you can import CommonJS modules using the syntax below (which is compliant with the ECMAScript specifications):

// my-module.ts
import foo from "someCommonJsModule";

For new TypeScript 3+ projects, the esModuleInterop setting is automatically enabled.

Renaming with export

It is also possible to use export statements to rename our exposed piece of code using the following syntax:

interface Mutation{ 
  content: string; 
  type: MutationType; 
}

enum MutationType {
  CreateTask = 'CREATE_TASK',
  ...
}
// 1
export {
    Mutation
}
// Renaming export
export {
    Mutation as RenamedMutation
}

In the examples above, the first export simply exports the symbol with its original name, while the second one exports it with a different name, using the as keyword.

Default export

Just as with the JavaScript ES6 modules, each module can have a default export using the following syntax.

// export default function
export default function log(mutation: Mutation): void {
    logToConsole(mutation);
}

The default export syntax can be used alongside the renamed export and export syntax as follows:

// export default function
export default function log(mutation: Mutation): void {
    logToConsole(mutation);
}

function saveMutation(mutation: Mutation): void {
    saveToLocalStorage(mutation);
}

export {
    saveMutation
}

// Renaming export
export {
    saveMutation as RenameSaveMutation
}

Finally, in a module, it is possible to export a single object or function using the export = ... syntax. For example:

//save-module.ts
function saveMutation(mutation: Mutation): void {
    saveToLocalStorage(mutation);
}
export = saveMutation;

Below is the corresponding import syntax when using the export = ... syntax:

//use-save-module.ts
import saveMutation = require("./save-module");
saveMutation(mutationToBeSave);

Please note, however, that this is not the recommended approach. You should only use this import style as a last resort to import an exposed piece of code from a module.

Re-exports

It is also possible to re-export exposed piece of code exported by other modules:

// module-with-re-exports.ts 
export * from "./module-1";

In the preceding code snippet, we did nothing in the module apart from re-exporting all the exposed pieces of code in module-1.ts.

How to use barrels with import and export

What are barrels?

Barrels are a technique used to roll up exports from different modules into a single one, usually called index.ts, to simplify the imports. Barrels thus simply combine the exports of one or more other modules. Barrels are incredibly useful because they let you concentrate on what piece of code you want to use and not on where they are located.

Understanding barrels in context

Barrels can be used tactically to ease the imports of a specific piece of exposed code.

For example, the code snippet below exports the books function, which returns bookList (an array of strings) using the export syntax:

//barrel/book-module.ts
const bookList: string = ["God of War", "Lord of the rings"]
export function books(): string[] {
  return bookList
}

Similarly, the following code snippet exports the cars function, which returns carList (an array of strings) using the export syntax.

//barrel/car-module.ts
const carList: string = ["Ferrari", "BMW"]
export function cars(): string[] {
  return carList
}

Finally, the following code snippet demonstrates how to use barrel to roll up exports from different modules into a single one, usually called index.ts. In this example, we re-export all of the exports from './book-module' and './car-module' respectively:

//barrel/index.ts
export * from './book-module';
export * from './car-module';

Why use barrels?

Without a barrel, a consumer would need two import statements:

//barrel/usage.ts
import { books } from '@/barrel/book-module';
import { cars } from '@/barrel/car-module';

Instead, barrels allow us to simplify imports, like so:

//barrel/usage.ts
import {book, car} from '@/barrel';
let allBooks = books(); //["God of War", "Lord of the rings"]
let allCars = cars(); //["Ferrari", "BMW"]

The code snippet below is also a valid barrel usage.

//barrel/usage.ts
import {book, car} from '@/barrel/index.ts';
let allBooks = books(); //["God of War", "Lord of the rings"]
let allCars = cars(); //["Ferrari", "BMW"]

Conclusion

The importance of modular programming cannot be overemphasized in general software development, although it is tempting to neglect it and allow the parts of our software to become deeply entangled.

In order to build scalable and reusable TypeScript applications, I recommend that you take advantage of TypeScript modules to improve the organization of your application. Doing so will increase code reusability and testability, and help create a better overall structure for your builds.

Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply