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.
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.
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:
In the following sections, we will discuss how to use modules with TypeScript.
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.
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.
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); }
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.
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.
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.
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.
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
.
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.
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';
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"]
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.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCreate a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.
Use CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.