Namespaces and Modules
This post outlines the various ways to organize your code using modules and namespaces in TypeScript. We’ll also go over some advanced topics of how to use namespaces and modules, and address some common pitfalls when using them in TypeScript.
See the Modules documentation for more information about ES Modules. See the Namespaces documentation for more information about TypeScript namespaces.
Note: In very old versions of TypeScript namespaces were called ‘Internal Modules’, these pre-date JavaScript module systems.
Modules can contain both code and declarations.
Modules also have a dependency on a module loader (such as CommonJs/Require.js) or a runtime which supports ES Modules. Modules provide for better code reuse, stronger isolation and better tooling support for bundling.
It is also worth noting that, for Node.js applications, modules are the default and we recommended modules over namespaces in modern code.
Starting with ECMAScript 2015, modules are native part of the language, and should be supported by all compliant engine implementations. Thus, for new projects modules would be the recommended code organization mechanism.
Namespaces are a TypeScript-specific way to organize code.
Namespaces are simply named JavaScript objects in the global namespace. This makes namespaces a very simple construct to use. Unlike modules, they can span multiple files, and can be concatenated using outFile . Namespaces can be a good way to structure your code in a Web Application, with all dependencies included as tags in your HTML page.
Just like all global namespace pollution, it can be hard to identify component dependencies, especially in a large application.
Pitfalls of Namespaces and Modules
In this section we’ll describe various common pitfalls in using namespaces and modules, and how to avoid them.
A common mistake is to try to use the /// syntax to refer to a module file, rather than using an import statement. To understand the distinction, we first need to understand how the compiler can locate the type information for a module based on the path of an import (e.g. the . in import x from «. «; , import x = require(«. «); , etc.) path.
The compiler will try to find a .ts , .tsx , and then a .d.ts with the appropriate path. If a specific file could not be found, then the compiler will look for an ambient module declaration. Recall that these need to be declared in a .d.ts file.
ts
// In a .d.ts file or .ts file that is not a module:declare module "SomeModule"export function fn(): string;>
ts
/// path="myModules.d.ts" />import * as m from "SomeModule";
The reference tag here allows us to locate the declaration file that contains the declaration for the ambient module. This is how the node.d.ts file that several of the TypeScript samples use is consumed.
If you’re converting a program from namespaces to modules, it can be easy to end up with a file that looks like this:
ts
export namespace Shapesexport class Triangle/* . */>export class Square/* . */>>
The top-level namespace here Shapes wraps up Triangle and Square for no reason. This is confusing and annoying for consumers of your module:
ts
import * as shapes from "./shapes";let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
A key feature of modules in TypeScript is that two different modules will never contribute names to the same scope. Because the consumer of a module decides what name to assign it, there’s no need to proactively wrap up the exported symbols in a namespace.
To reiterate why you shouldn’t try to namespace your module contents, the general idea of namespacing is to provide logical grouping of constructs and to prevent name collisions. Because the module file itself is already a logical grouping, and its top-level name is defined by the code that imports it, it’s unnecessary to use an additional module layer for exported objects.
ts
export class Triangle/* . */>export class Square/* . */>
ts
import * as shapes from "./shapes";let t = new shapes.Triangle();
Just as there is a one-to-one correspondence between JS files and modules, TypeScript has a one-to-one correspondence between module source files and their emitted JS files. One effect of this is that it’s not possible to concatenate multiple module source files depending on the module system you target. For instance, you can’t use the outFile option while targeting commonjs or umd , but with TypeScript 1.8 and later, it’s possible to use outFile when targeting amd or system .
The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request ❤
Modules
JavaScript has a long history of different ways to handle modularizing code. Having been around since 2012, TypeScript has implemented support for a lot of these formats, but over time the community and the JavaScript specification has converged on a format called ES Modules (or ES6 modules). You might know it as the import / export syntax.
ES Modules was added to the JavaScript spec in 2015, and by 2020 had broad support in most web browsers and JavaScript runtimes.
For focus, the handbook will cover both ES Modules and its popular pre-cursor CommonJS module.exports = syntax, and you can find information about the other module patterns in the reference section under Modules.
How JavaScript Modules are Defined
In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.
Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).
Modules are executed within their own scope, not in the global scope. This means that variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported using one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one of the import forms.
Before we start, it’s important to understand what TypeScript considers a module. The JavaScript specification declares that any JavaScript files without an import declaration, export , or top-level await should be considered a script and not a module.
Inside a script file variables and types are declared to be in the shared global scope, and it’s assumed that you’ll either use the outFile compiler option to join multiple input files into one output file, or use multiple tags in your HTML to load these files (in the correct order!).
If you have a file that doesn’t currently have any import s or export s, but you want to be treated as a module, add the line:
which will change the file to be a module exporting nothing. This syntax works regardless of your module target.
There are three main things to consider when writing module-based code in TypeScript:
- Syntax: What syntax do I want to use to import and export things?
- Module Resolution: What is the relationship between module names (or paths) and files on disk?
- Module Output Target: What should my emitted JavaScript module look like?
A file can declare a main export via export default :
This is then imported via:
In addition to the default export, you can have more than one export of variables and functions via the export by omitting default :
These can be used in another file via the import syntax:
An import can be renamed using a format like import :
You can mix and match the above syntax into a single import :
You can take all of the exported objects and put them into a single namespace using * as name :
You can import a file and not include any variables into your current module via import «./file» :
In this case, the import does nothing. However, all of the code in maths.ts was evaluated, which could trigger side-effects which affect other objects.
TypeScript Specific ES Module Syntax
Types can be exported and imported using the same syntax as JavaScript values:
TypeScript has extended the import syntax with two concepts for declaring an import of a type:
Which is an import statement which can only import types:
TypeScript 4.5 also allows for individual imports to be prefixed with type to indicate that the imported reference is a type:
Together these allow a non-TypeScript transpiler like Babel, swc or esbuild to know what imports can be safely removed.
ES Module Syntax with CommonJS Behavior
TypeScript has ES Module syntax which directly correlates to a CommonJS and AMD require . Imports using ES Module are for most cases the same as the require from those environments, but this syntax ensures you have a 1 to 1 match in your TypeScript file with the CommonJS output:
You can learn more about this syntax in the modules reference page.
CommonJS is the format which most modules on npm are delivered in. Even if you are writing using the ES Modules syntax above, having a brief understanding of how CommonJS syntax works will help you debug easier.
Identifiers are exported via setting the exports property on a global called module .
Then these files can be imported via a require statement:
Or you can simplify a bit using the destructuring feature in JavaScript:
CommonJS and ES Modules interop
There is a mis-match in features between CommonJS and ES Modules regarding the distinction between a default import and a module namespace object import. TypeScript has a compiler flag to reduce the friction between the two different sets of constraints with esModuleInterop .
TypeScript’s Module Resolution Options
Module resolution is the process of taking a string from the import or require statement, and determining what file that string refers to.
TypeScript includes two resolution strategies: Classic and Node. Classic, the default when the compiler option module is not commonjs , is included for backwards compatibility. The Node strategy replicates how Node.js works in CommonJS mode, with additional checks for .ts and .d.ts .
There are many TSConfig flags which influence the module strategy within TypeScript: moduleResolution , baseUrl , paths , rootDirs .
For the full details on how these strategies work, you can consult the Module Resolution.
TypeScript’s Module Output Options
There are two options which affect the emitted JavaScript output:
- target which determines which JS features are downleveled (converted to run in older JavaScript runtimes) and which are left intact
- module which determines what code is used for modules to interact with each other
Which target you use is determined by the features available in the JavaScript runtime you expect to run the TypeScript code in. That could be: the oldest web browser you support, the lowest version of Node.js you expect to run on or could come from unique constraints from your runtime — like Electron for example.
All communication between modules happens via a module loader, the compiler option module determines which one is used. At runtime the module loader is responsible for locating and executing all dependencies of a module before executing it.
For example, here is a TypeScript file using ES Modules syntax, showcasing a few different options for module :