Was this page helpful?

Modules - Reference

Module syntax

The TypeScript compiler recognizes standardECMAScript module syntax in TypeScript and JavaScript files and many forms ofCommonJS syntax in JavaScript files.

There are also a few TypeScript-specific syntax extensions that can be used in TypeScript files and/or JSDoc comments.

Importing and exporting TypeScript-specific declarations

Type aliases, interfaces, enums, and namespaces can be exported from a module with anexport modifier, like any standard JavaScript declaration:

ts
// Standard JavaScript syntax...
exportfunctionf() {}
// ...extended to type declarations
exporttypeSomeType =/* ... */;
exportinterfaceSomeInterface {/* ... */ }

They can also be referenced in named exports, even alongside references to standard JavaScript declarations:

ts
export {f,SomeType,SomeInterface };

Exported types (and other TypeScript-specific declarations) can be imported with standard ECMAScript imports:

ts
import {f,SomeType,SomeInterface }from"./module.js";

When using namespace imports or exports, exported types are available on the namespace when referenced in a type position:

ts
import*asmodfrom"./module.js";
mod.f();
mod.SomeType;// Property 'SomeType' does not exist on type 'typeof import("./module.js")'
letx:mod.SomeType;// Ok

Type-only imports and exports

When emitting imports and exports to JavaScript, by default, TypeScript automatically elides (does not emit) imports that are only used in type positions and exports that only refer to types. Type-only imports and exports can be used to force this behavior and make the elision explicit. Import declarations written withimport type, export declarations written withexport type { ... }, and import or export specifiers prefixed with thetype keyword are all guaranteed to be elided from the output JavaScript.

ts
// @Filename: main.ts
import {f,typeSomeInterface }from"./module.js";
importtype {SomeType }from"./module.js";
classCimplementsSomeInterface {
constructor(p:SomeType) {
f();
}
}
exporttype {C };
// @Filename: main.js
import {f }from"./module.js";
classC {
constructor(p) {
f();
}
}

Even values can be imported withimport type, but since they won’t exist in the output JavaScript, they can only be used in non-emitting positions:

ts
importtype {f }from"./module.js";
f();// 'f' cannot be used as a value because it was imported using 'import type'
letotherFunction:typeoff = ()=> {};// Ok

A type-only import declaration may not declare both a default import and named bindings, since it appears ambiguous whethertype applies to the default import or to the entire import declaration. Instead, split the import declaration into two, or usedefault as a named binding:

ts
importtypefs, {BigIntOptions }from"fs";
// ^^^^^^^^^^^^^^^^^^^^^
// Error: A type-only import can specify a default import or named bindings, but not both.
importtype {defaultasfs,BigIntOptions }from"fs";// Ok

import() types

TypeScript provides a type syntax similar to JavaScript’s dynamicimport for referencing the type of a module without writing an import declaration:

ts
// Access an exported type:
typeWriteFileOptions =import("fs").WriteFileOptions;
// Access the type of an exported value:
typeWriteFileFunction =typeofimport("fs").writeFile;

This is especially useful in JSDoc comments in JavaScript files, where it’s not possible to import types otherwise:

ts
/**@type{import("webpack").Configuration} */
module.exports = {
// ...
}

export = andimport = require()

When emitting CommonJS modules, TypeScript files can use a direct analog ofmodule.exports = ... andconst mod = require("...") JavaScript syntax:

ts
// @Filename: main.ts
importfs =require("fs");
export =fs.readFileSync("...");
// @Filename: main.js
"use strict";
constfs =require("fs");
module.exports =fs.readFileSync("...");

This syntax was used over its JavaScript counterparts since variable declarations and property assignments could not refer to TypeScript types, whereas special TypeScript syntax could:

ts
// @Filename: a.ts
interfaceOptions {/* ... */ }
module.exports =Options;// Error: 'Options' only refers to a type, but is being used as a value here.
export =Options;// Ok
// @Filename: b.ts
constOptions =require("./a");
constoptions:Options = {/* ... */ };// Error: 'Options' refers to a value, but is being used as a type here.
// @Filename: c.ts
importOptions =require("./a");
constoptions:Options = {/* ... */ };// Ok

Ambient modules

TypeScript supports a syntax in script (non-module) files for declaring a module that exists in the runtime but has no corresponding file. Theseambient modules usually represent runtime-provided modules, like"fs" or"path" in Node.js:

ts
declaremodule"path" {
exportfunctionnormalize(p:string):string;
exportfunctionjoin(...paths:any[]):string;
exportvarsep:string;
}

Once an ambient module is loaded into a TypeScript program, TypeScript will recognize imports of the declared module in other files:

ts
// 👇 Ensure the ambient module is loaded -
// may be unnecessary if path.d.ts is included
// by the project tsconfig.json somehow.
///<referencepath="path.d.ts"/>
import {normalize,join }from"path";

Ambient module declarations are easy to confuse withmodule augmentations since they use identical syntax. This module declaration syntax becomes a module augmentation when the file is a module, meaning it has a top-levelimport orexport statement (or is affected by--moduleDetection force orauto):

ts
// Not an ambient module declaration anymore!
export {};
declaremodule"path" {
exportfunctionnormalize(p:string):string;
exportfunctionjoin(...paths:any[]):string;
exportvarsep:string;
}

Ambient modules may use imports inside the module declaration body to refer to other modules without turning the containing file into a module (which would make the ambient module declaration a module augmentation):

ts
declaremodule"m" {
// Moving this outside "m" would totally change the meaning of the file!
import {SomeType }from"other";
exportfunctionf():SomeType;
}

Apattern ambient module contains a single* wildcard character in its name, matching zero or more characters in import paths. This can be useful for declaring modules provided by custom loaders:

ts
declaremodule"*.html" {
constcontent:string;
exportdefaultcontent;
}

Themodule compiler option

This section discusses the details of eachmodule compiler option value. See theModule output format theory section for more background on what the option is and how it fits into the overall compilation process. In brief, themodule compiler option was historically only used to control the output module format of emitted JavaScript files. The more recentnode16,node18, andnodenext values, however, describe a wide range of characteristics of Node.js’s module system, including what module formats are supported, how the module format of each file is determined, and how different module formats interoperate.

node16,node18,nodenext

Node.js supports both CommonJS and ECMAScript modules, with specific rules for which format each file can be and how the two formats are allowed to interoperate.node16,node18, andnodenext describe the full range of behavior for Node.js’s dual-format module system, andemit files in either CommonJS or ESM format. This is different from every othermodule option, which are runtime-agnostic and force all output files into a single format, leaving it to the user to ensure the output is valid for their runtime.

A common misconception is thatnode16nodenext only emit ES modules. In reality, these modes describe versions of Node.js thatsupport ES modules, not just projects thatuse ES modules. Both ESM and CommonJS emit are supported, based on thedetected module format of each file. Because they are the onlymodule options that reflect the complexities of Node.js’s dual module system, they are theonly correctmodule options for all apps and libraries that are intended to run in Node.js v12 or later, whether they use ES modules or not.

The fixed-versionnode16 andnode18 modes represent the module system behavior stabilized in their respective Node.js versions, while thenodenext mode changes with the latest stable versions of Node.js. The following table summarizes the current differences between the three modes:

targetmoduleResolutionimport assertionsimport attributesJSON importsrequire(esm)
node16es2022node16no restrictions
node18es2022node16needstype "json"
nodenextesnextnodenextneedstype "json"

Module format detection

  • .mts/.mjs/.d.mts files are always ES modules.
  • .cts/.cjs/.d.cts files are always CommonJS modules.
  • .ts/.tsx/.js/.jsx/.d.ts files are ES modules if the nearest ancestor package.json file contains"type": "module", otherwise CommonJS modules.

The detected module format of input.ts/.tsx/.mts/.cts files determines the module format of the emitted JavaScript files. So, for example, a project consisting entirely of.ts files will emit all CommonJS modules by default under--module nodenext, and can be made to emit all ES modules by adding"type": "module" to the project package.json.

Interoperability rules

  • When an ES module references a CommonJS module:
    • Themodule.exports of the CommonJS module is available as a default import to the ES module.
    • Properties (other thandefault) of the CommonJS module’smodule.exports may or may not be available as named imports to the ES module. Node.js attempts to make them available viastatic analysis. TypeScript cannot know from a declaration file whether that static analysis will succeed, and optimistically assumes it will. This limits TypeScript’s ability to catch named imports that may crash at runtime. See#54018 for more details.
  • When a CommonJS module references an ES module:
    • Innode16 andnode18,require cannot reference an ES module. For TypeScript, this includesimport statements in files that aredetected to be CommonJS modules, since thoseimport statements will be transformed torequire calls in the emitted JavaScript.
    • Innodenext, to reflect the behavior of Node.js v22.12.0 and later,require can reference an ES module. In Node.js, an error is thrown if the ES module, or any of its imported modules, uses top-levelawait. TypeScript does not attempt to detect this case and will not emit a compile-time error. The result of therequire call is the module’s Module Namespace Object, i.e., the same as the result of anawait import() of the same module (but without the need toawait anything).
    • A dynamicimport() call can always be used to import an ES module. It returns a Promise of the module’s Module Namespace Object (what you’d get fromimport * as ns from "./module.js" from another ES module).

Emit

The emit format of each file is determined by thedetected module format of each file. ESM emit is similar to--module esnext, but has a special transformation forimport x = require("..."), which is not allowed in--module esnext:

ts
// @Filename: main.ts
importx =require("mod");
js
// @Filename: main.js
import {createRequireas_createRequire }from"module";
const__require =_createRequire(import.meta.url);
constx =__require("mod");

CommonJS emit is similar to--module commonjs, but dynamicimport() calls are not transformed. Emit here is shown withesModuleInterop enabled:

ts
// @Filename: main.ts
importfsfrom"fs";// transformed
constdynamic =import("mod");// not transformed
js
// @Filename: main.js
"use strict";
var__importDefault = (this &&this.__importDefault) ||function (mod) {
return (mod &&mod.__esModule) ?mod : {"default":mod };
};
Object.defineProperty(exports,"__esModule", {value:true });
constfs_1 =__importDefault(require("fs"));// transformed
constdynamic =import("mod");// not transformed

Implied and enforced options

  • --module nodenext implies and enforces--moduleResolution nodenext.
  • --module node18 ornode16 implies and enforces--moduleResolution node16.
  • --module nodenext implies--target esnext.
  • --module node18 ornode16 implies--target es2022.
  • --module nodenext ornode18 ornode16 implies--esModuleInterop.

Summary

  • node16,node18, andnodenext are the only correctmodule options for all apps and libraries that are intended to run in Node.js v12 or later, whether they use ES modules or not.
  • node16,node18, andnodenext emit files in either CommonJS or ESM format, based on thedetected module format of each file.
  • Node.js’s interoperability rules between ESM and CJS are reflected in type checking.
  • ESM emit transformsimport x = require("...") to arequire call constructed from acreateRequire import.
  • CommonJS emit leaves dynamicimport() calls untransformed, so CommonJS modules can asynchronously import ES modules.

preserve

In--module preserve (added in TypeScript 5.4), ECMAScript imports and exports written in input files are preserved in the output, and CommonJS-styleimport x = require("...") andexport = ... statements are emitted as CommonJSrequire andmodule.exports. In other words, the format of each individual import or export statement is preserved, rather than being coerced into a single format for the whole compilation (or even a whole file).

While it’s rare to need to mix imports and require calls in the same file, thismodule mode best reflects the capabilities of most modern bundlers, as well as the Bun runtime.

Why care about TypeScript’smodule emit with a bundler or with Bun, where you’re likely also settingnoEmit? TypeScript’s type checking and module resolution behavior are affected by the module format that itwould emit. Settingmodule gives TypeScript information about how your bundler or runtime will process imports and exports, which ensures that the types you see on imported values accurately reflect what will happen at runtime or after bundling. See--moduleResolution bundler for more discussion.

Examples

ts
// @Filename: main.ts
importx, {y,z }from"mod";
importmod =require("mod");
constdynamic =import("mod");
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
importx, {y,z }from"mod";
constmod =require("mod");
constdynamic =import("mod");
exportconste1 =0;
exportdefault"default export";

Implied and enforced options

  • --module preserve implies--moduleResolution bundler.
  • --module preserve implies--esModuleInterop.

The option--esModuleInterop is enabled by default in--module preserve only for itstype checking behavior. Since imports never transform into require calls in--module preserve,--esModuleInterop does not affect the emitted JavaScript.

es2015,es2020,es2022,esnext

Summary

  • Useesnext with--moduleResolution bundler for bundlers, Bun, and tsx.
  • Do not use for Node.js. Usenode16,node18, ornodenext with"type": "module" in package.json to emit ES modules for Node.js.
  • import mod = require("mod") is not allowed in non-declaration files.
  • es2020 adds support forimport.meta properties.
  • es2022 adds support for top-levelawait.
  • esnext is a moving target that may include support for Stage 3 proposals to ECMAScript modules.
  • Emitted files are ES modules, but dependencies may be any format.

Examples

ts
// @Filename: main.ts
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";

commonjs

Summary

  • You probably shouldn’t use this. Usenode16,node18, ornodenext to emit CommonJS modules for Node.js.
  • Emitted files are CommonJS modules, but dependencies may be any format.
  • Dynamicimport() is transformed to a Promise of arequire() call.
  • esModuleInterop affects the output code for default and namespace imports.

Examples

Output is shown withesModuleInterop: false.

ts
// @Filename: main.ts
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
"use strict";
Object.defineProperty(exports,"__esModule", {value:true });
exports.e1 =void0;
constmod_1 =require("mod");
constmod =require("mod");
constdynamic =Promise.resolve().then(()=>require("mod"));
console.log(mod_1.default,mod_1.y,mod_1.z,mod);
exports.e1 =0;
exports.default ="default export";
ts
// @Filename: main.ts
importmod =require("mod");
console.log(mod);
export = {
p1:true,
p2:false
};
js
// @Filename: main.js
"use strict";
constmod =require("mod");
console.log(mod);
module.exports = {
p1:true,
p2:false
};

system

Summary

Examples

ts
// @Filename: main.ts
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
System.register(["mod"],function (exports_1,context_1) {
"use strict";
varmod_1,mod,dynamic,e1;
var__moduleName =context_1 &&context_1.id;
return {
setters: [
function (mod_1_1) {
mod_1 =mod_1_1;
mod =mod_1_1;
}
],
execute:function () {
dynamic =context_1.import("mod");
console.log(mod_1.default,mod_1.y,mod_1.z,mod,dynamic);
exports_1("e1",e1 =0);
exports_1("default","default export");
}
};
});

amd

Summary

  • Designed for AMD loaders like RequireJS.
  • You probably shouldn’t use this. Use a bundler instead.
  • Emitted files are AMD modules, but dependencies may be any format.
  • SupportsoutFile.

Examples

ts
// @Filename: main.ts
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
define(["require","exports","mod","mod"],function (require,exports,mod_1,mod) {
"use strict";
Object.defineProperty(exports,"__esModule", {value:true });
exports.e1 =void0;
constdynamic =newPromise((resolve_1,reject_1)=> {require(["mod"],resolve_1,reject_1); });
console.log(mod_1.default,mod_1.y,mod_1.z,mod,dynamic);
exports.e1 =0;
exports.default ="default export";
});

umd

Summary

  • Designed for AMD or CommonJS loaders.
  • Does not expose a global variable like most other UMD wrappers.
  • You probably shouldn’t use this. Use a bundler instead.
  • Emitted files are UMD modules, but dependencies may be any format.

Examples

ts
// @Filename: main.ts
importx, {y,z }from"mod";
import*asmodfrom"mod";
constdynamic =import("mod");
console.log(x,y,z,mod,dynamic);
exportconste1 =0;
exportdefault"default export";
js
// @Filename: main.js
(function (factory) {
if (typeofmodule ==="object" &&typeofmodule.exports ==="object") {
varv =factory(require,exports);
if (v !==undefined)module.exports =v;
}
elseif (typeofdefine ==="function" &&define.amd) {
define(["require","exports","mod","mod"],factory);
}
})(function (require,exports) {
"use strict";
var__syncRequire =typeofmodule ==="object" &&typeofmodule.exports ==="object";
Object.defineProperty(exports,"__esModule", {value:true });
exports.e1 =void0;
constmod_1 =require("mod");
constmod =require("mod");
constdynamic =__syncRequire ?Promise.resolve().then(()=>require("mod")) :newPromise((resolve_1,reject_1)=> {require(["mod"],resolve_1,reject_1); });
console.log(mod_1.default,mod_1.y,mod_1.z,mod,dynamic);
exports.e1 =0;
exports.default ="default export";
});

ThemoduleResolution compiler option

This section describes module resolution features and processes shared by multiplemoduleResolution modes, then specifies the details of each mode. See theModule resolution theory section for more background on what the option is and how it fits into the overall compilation process. In brief,moduleResolution controls how TypeScript resolvesmodule specifiers (string literals inimport/export/require statements) to files on disk, and should be set to match the module resolver used by the target runtime or bundler.

Common features and processes

File extension substitution

TypeScript always wants to resolve internally to a file that can provide type information, while ensuring that the runtime or bundler can use the same path to resolve to a file that provides a JavaScript implementation. For any module specifier that would, according to themoduleResolution algorithm specified, trigger a lookup of a JavaScript file in the runtime or bundler, TypeScript will first try to find a TypeScript implementation file or type declaration file with the same name and analagous file extension.

Runtime lookupTypeScript lookup #1TypeScript lookup #2TypeScript lookup #3TypeScript lookup #4TypeScript lookup #5
/mod.js/mod.ts/mod.tsx/mod.d.ts/mod.js./mod.jsx
/mod.mjs/mod.mts/mod.d.mts/mod.mjs
/mod.cjs/mod.cts/mod.d.cts/mod.cjs

Note that this behavior is independent of the actual module specifier written in the import. This means that TypeScript can resolve to a.ts or.d.ts file even if the module specifier explicitly uses a.js file extension:

ts
importxfrom"./mod.js";
// Runtime lookup: "./mod.js"
// TypeScript lookup #1: "./mod.ts"
// TypeScript lookup #2: "./mod.d.ts"
// TypeScript lookup #3: "./mod.js"

SeeTypeScript imitates the host’s module resolution, but with types for an explanation of why TypeScript’s module resolution works this way.

Relative file path resolution

All of TypeScript’smoduleResolution algorithms support referencing a module by a relative path that includes a file extension (which will be substituted according to therules above):

ts
// @Filename: a.ts
export {};
// @Filename: b.ts
import {}from"./a.js";// ✅ Works in every `moduleResolution`

Extensionless relative paths

In some cases, the runtime or bundler allows omitting a.js file extension from a relative path. TypeScript supports this behavior where themoduleResolution setting and the context indicate that the runtime or bundler supports it:

ts
// @Filename: a.ts
export {};
// @Filename: b.ts
import {}from"./a";

If TypeScript determines that the runtime will perform a lookup for./a.js given the module specifier"./a", then./a.js will undergoextension substitution, and resolve to the filea.ts in this example.

Extensionless relative paths are not supported inimport paths in Node.js, and are not always supported in file paths specified in package.json files. TypeScript currently never supports omitting a.mjs/.mts or.cjs/.cts file extension, even though some runtimes and bundlers do.

Directory modules (index file resolution)

In some cases, a directory, rather than a file, can be referenced as a module. In the simplest and most common case, this involves the runtime or bundler looking for anindex.js file in a directory. TypeScript supports this behavior where themoduleResolution setting and the context indicate that the runtime or bundler supports it:

ts
// @Filename: dir/index.ts
export {};
// @Filename: b.ts
import {}from"./dir";

If TypeScript determines that the runtime will perform a lookup for./dir/index.js given the module specifier"./dir", then./dir/index.js will undergoextension substitution, and resolve to the filedir/index.ts in this example.

Directory modules may also contain a package.json file, where resolution of the"main" and"types" fields are supported, and take precedence overindex.js lookups. The"typesVersions" field is also supported in directory modules.

Note that directory modules are not the same asnode_modules packages and only support a subset of the features available to packages, and are not supported at all in some contexts. Node.js considers them alegacy feature.

paths

Overview

TypeScript offers a way to override the compiler’s module resolution for bare specifiers with thepaths compiler option. While the feature was originally designed to be used with the AMD module loader (a means of running modules in the browser before ESM existed or bundlers were widely used), it still has uses today when a runtime or bundler supports module resolution features that TypeScript does not model. For example, when running Node.js with--experimental-network-imports, you can manually specify a local type definition file for a specifichttps:// import:

json
{
"compilerOptions": {
"module":"nodenext",
"paths": {
"https://esm.sh/lodash@4.17.21": ["./node_modules/@types/lodash/index.d.ts"]
}
}
}
ts
// Typed by ./node_modules/@types/lodash/index.d.ts due to `paths` entry
import {add }from"https://esm.sh/lodash@4.17.21";

It’s also common for apps built with bundlers to define convenience path aliases in their bundler configuration, and then inform TypeScript of those aliases withpaths:

json
{
"compilerOptions": {
"module":"esnext",
"moduleResolution":"bundler",
"paths": {
"@app/*": ["./src/*"]
}
}
}
paths does not affect emit

Thepaths option doesnot change the import path in the code emitted by TypeScript. Consequently, it’s very easy to create path aliases that appear to work in TypeScript but will crash at runtime:

json
{
"compilerOptions": {
"module":"nodenext",
"paths": {
"node-has-no-idea-what-this-is": ["./oops.ts"]
}
}
}
ts
// TypeScript: ✅
// Node.js: 💥
import {}from"node-has-no-idea-what-this-is";

While it’s ok for bundled apps to set uppaths, it’s very important that published libraries donot, since the emitted JavaScript will not work for consumers of the library without those users setting up the same aliases for both TypeScript and their bundler. Both libraries and apps can considerpackage.json"imports" as a standard replacement for conveniencepaths aliases.

paths should not point to monorepo packages or node_modules packages

While module specifiers that matchpaths aliases are bare specifiers, once the alias is resolved, module resolution proceeds on the resolved path as a relative path. Consequently, resolution features that happen fornode_modules package lookups, including package.json"exports" field support, do not take effect when apaths alias is matched. This can lead to surprising behavior ifpaths is used to point to anode_modules package:

ts
{
"compilerOptions": {
"paths": {
"pkg": ["./node_modules/pkg/dist/index.d.ts"],
"pkg/*": ["./node_modules/pkg/*"]
}
}
}

While this configuration may simulate some of the behavior of package resolution, it overrides anymain,types,exports, andtypesVersions the package’spackage.json file defines, and imports from the package may fail at runtime.

The same caveat applies to packages referencing each other in a monorepo. Instead of usingpaths to make TypeScript artificially resolve"@my-scope/lib" to a sibling package, it’s best to use workspaces vianpm,yarn, orpnpm to symlink your packages intonode_modules, so both TypeScript and the runtime or bundler perform realnode_modules package lookups. This is especially important if the monorepo packages will be published to npm—the packages will reference each other vianode_modules package lookups once installed by users, and using workspaces allows you to test that behavior during local development.

Relationship tobaseUrl

WhenbaseUrl is provided, the values in eachpaths array are resolved relative to thebaseUrl. Otherwise, they are resolved relative to thetsconfig.json file that defines them.

Wildcard substitutions

paths patterns can contain a single* wildcard, which matches any string. The* token can then be used in the file path values to substitute the matched string:

json
{
"compilerOptions": {
"paths": {
"@app/*": ["./src/*"]
}
}
}

When resolving an import of"@app/components/Button", TypeScript will match on@app/*, binding* tocomponents/Button, and then attempt to resolve the path./src/components/Button relative to thetsconfig.json path. The remainder of this lookup will follow the same rules as any otherrelative path lookup according to themoduleResolution setting.

When multiple patterns match a module specifier, the pattern with the longest matching prefix before any* token is used:

json
{
"compilerOptions": {
"paths": {
"*": ["./src/foo/one.ts"],
"foo/*": ["./src/foo/two.ts"],
"foo/bar": ["./src/foo/three.ts"]
}
}
}

When resolving an import of"foo/bar", all threepaths patterns match, but the last is used because"foo/bar" is longer than"foo/" and"".

Fallbacks

Multiple file paths can be provided for a path mapping. If resolution fails for one path, the next one in the array will be attempted until resolution succeeds or the end of the array is reached.

json
{
"compilerOptions": {
"paths": {
"*": ["./vendor/*","./types/*"]
}
}
}

baseUrl

baseUrl was designed for use with AMD module loaders. If you aren’t using an AMD module loader, you probably shouldn’t usebaseUrl. Since TypeScript 4.1,baseUrl is no longer required to usepaths and should not be used just to set the directorypaths values are resolved from.

ThebaseUrl compiler option can be combined with anymoduleResolution mode and specifies a directory that bare specifiers (module specifiers that don’t begin with./,../, or/) are resolved from.baseUrl has a higher precedence thannode_modules package lookups inmoduleResolution modes that support them.

When performing abaseUrl lookup, resolution proceeds with the same rules as other relative path resolutions. For example, in amoduleResolution mode that supportsextensionless relative paths a module specifier"some-file" may resolve to/src/some-file.ts ifbaseUrl is set to/src.

Resolution of relative module specifiers are never affected by thebaseUrl option.

node_modules package lookups

Node.js treats module specifiers that aren’t relative paths, absolute paths, or URLs as references to packages that it looks up innode_modules subdirectories. Bundlers conveniently adopted this behavior to allow their users to use the same dependency management system, and often even the same dependencies, as they would in Node.js. All of TypeScript’smoduleResolution options exceptclassic supportnode_modules lookups. (classic supports lookups innode_modules/@types when other means of resolution fail, but never looks for packages innode_modules directly.) Everynode_modules package lookup has the following structure (beginning after higher precedence bare specifier rules, likepaths,baseUrl, self-name imports, and package.json"imports" lookups have been exhausted):

  1. For each ancestor directory of the importing file, if anode_modules directory exists within it:
    1. If a directory with the same name as the package exists withinnode_modules:
      1. Attempt to resolve types from the package directory.
      2. If a result is found, return it and stop the search.
    2. If a directory with the same name as the package exists withinnode_modules/@types:
      1. Attempt to resolve types from the@types package directory.
      2. If a result is found, return it and stop the search.
  2. Repeat the previous search through allnode_modules directories, but this time, allow JavaScript files as a result, and do not search in@types directories.

AllmoduleResolution modes (exceptclassic) follow this pattern, while the details of how they resolve from a package directory, once located, differ, and are explained in the following sections.

package.json"exports"

WhenmoduleResolution is set tonode16,nodenext, orbundler, andresolvePackageJsonExports is not disabled, TypeScript follows Node.js’spackage.json"exports" spec when resolving from a package directory triggered by abare specifiernode_modules package lookup.

TypeScript’s implementation for resolving a module specifier through"exports" to a file path follows Node.js exactly. Once a file path is resolved, however, TypeScript will stilltry multiple file extensions in order to prioritize finding types.

When resolving throughconditional"exports", TypeScript always matches the"types" and"default" conditions if present. Additionally, TypeScript will match a versioned types condition in the form"types@{selector}" (where{selector} is a"typesVersions"-compatible version selector) according to the same version-matching rules implemented in"typesVersions". Other non-configurable conditions are dependent on themoduleResolution mode and specified in the following sections. Additional conditions can be configured to match with thecustomConditions compiler option.

Note that the presence of"exports" prevents any subpaths not explicitly listed or matched by a pattern in"exports" from being resolved.

Example: subpaths, conditions, and extension substitution

Scenario:"pkg/subpath" is requested with conditions["types", "node", "require"] (determined bymoduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:

json
{
"name":"pkg",
"exports": {
".": {
"import":"./index.mjs",
"require":"./index.cjs"
},
"./subpath": {
"import":"./subpath/index.mjs",
"require":"./subpath/index.cjs"
}
}
}

Resolution process within the package directory:

  1. Does"exports" exist?Yes.
  2. Does"exports" have a"./subpath" entry?Yes.
  3. The value atexports["./subpath"] is an object—it must be specifying conditions.
  4. Does the first condition"import" match this request?No.
  5. Does the second condition"require" match this request?Yes.
  6. Does the path"./subpath/index.cjs" have a recognized TypeScript file extension?No, so use extension substitution.
  7. Viaextension substitution, try the following paths, returning the first one that exists, orundefined otherwise:
    1. ./subpath/index.cts
    2. ./subpath/index.d.cts
    3. ./subpath/index.cjs

If./subpath/index.cts or./subpath.d.cts exists, resolution is complete. Otherwise, resolution searchesnode_modules/@types/pkg and othernode_modules directories in an attempt to resolve types, according to thenode_modules package lookups rules. If no types are found, a second pass through allnode_modules resolves to./subpath/index.cjs (assuming it exists), which counts as a successful resolution, but one that does not provide types, leading toany-typed imports and anoImplicitAny error if enabled.

Example: explicit"types" condition

Scenario:"pkg/subpath" is requested with conditions["types", "node", "import"] (determined bymoduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:

json
{
"name":"pkg",
"exports": {
"./subpath": {
"import": {
"types":"./types/subpath/index.d.mts",
"default":"./es/subpath/index.mjs"
},
"require": {
"types":"./types/subpath/index.d.cts",
"default":"./cjs/subpath/index.cjs"
}
}
}
}

Resolution process within the package directory:

  1. Does"exports" exist?Yes.
  2. Does"exports" have a"./subpath" entry?Yes.
  3. The value atexports["./subpath"] is an object—it must be specifying conditions.
  4. Does the first condition"import" match this request?Yes.
  5. The value atexports["./subpath"].import is an object—it must be specifying conditions.
  6. Does the first condition"types" match this request?Yes.
  7. Does the path"./types/subpath/index.d.mts" have a recognized TypeScript file extension?Yes, so don’t use extension substitution.
  8. Return the path"./types/subpath/index.d.mts" if the file exists,undefined otherwise.
Example: versioned"types" condition

Scenario: using TypeScript 4.7.5,"pkg/subpath" is requested with conditions["types", "node", "import"] (determined bymoduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:

json
{
"name":"pkg",
"exports": {
"./subpath": {
"types@>=5.2":"./ts5.2/subpath/index.d.ts",
"types@>=4.6":"./ts4.6/subpath/index.d.ts",
"types":"./tsold/subpath/index.d.ts",
"default":"./dist/subpath/index.js"
}
}
}

Resolution process within the package directory:

  1. Does"exports" exist?Yes.
  2. Does"exports" have a"./subpath" entry?Yes.
  3. The value atexports["./subpath"] is an object—it must be specifying conditions.
  4. Does the first condition"types@>=5.2" match this request?No, 4.7.5 is not greater than or equal to 5.2.
  5. Does the second condition"types@>=4.6" match this request?Yes, 4.7.5 is greater than or equal to 4.6.
  6. Does the path"./ts4.6/subpath/index.d.ts" have a recognized TypeScript file extension?Yes, so don’t use extension substitution.
  7. Return the path"./ts4.6/subpath/index.d.ts" if the file exists,undefined otherwise.
Example: subpath patterns

Scenario:"pkg/wildcard.js" is requested with conditions["types", "node", "import"] (determined bymoduleResolution setting and the context that triggered the module resolution request) in a package directory with the following package.json:

json
{
"name":"pkg",
"type":"module",
"exports": {
"./*.js": {
"types":"./types/*.d.ts",
"default":"./dist/*.js"
}
}
}

Resolution process within the package directory:

  1. Does"exports" exist?Yes.
  2. Does"exports" have a"./wildcard.js" entry?No.
  3. Does any key with a* in it match"./wildcard.js"?Yes,"./*.js" matches and setswildcard to be the substitution.
  4. The value atexports["./*.js"] is an object—it must be specifying conditions.
  5. Does the first condition"types" match this request?Yes.
  6. In./types/*.d.ts, replace* with the substitutionwildcard../types/wildcard.d.ts
  7. Does the path"./types/wildcard.d.ts" have a recognized TypeScript file extension?Yes, so don’t use extension substitution.
  8. Return the path"./types/wildcard.d.ts" if the file exists,undefined otherwise.
Example:"exports" block other subpaths

Scenario:"pkg/dist/index.js" is requested in a package directory with the following package.json:

json
{
"name":"pkg",
"main":"./dist/index.js",
"exports":"./dist/index.js"
}

Resolution process within the package directory:

  1. Does"exports" exist?Yes.
  2. The value atexports is a string—it must be a file path for the package root (".").
  3. Is the request"pkg/dist/index.js" for the package root?No, it has a subpathdist/index.js.
  4. Resolution fails; returnundefined.

Without"exports", the request could have succeeded, but the presence of"exports" prevents resolving any subpaths that cannot be matched through"exports".

package.json"typesVersions"

Anode_modules package ordirectory module may specify a"typesVersions" field in its package.json to redirect TypeScript’s resolution process according to the TypeScript compiler version, and fornode_modules packages, according to the subpath being resolved. This allows package authors to include new TypeScript syntax in one set of type definitions while providing another set for backward compatibility with older TypeScript versions (through a tool likedownlevel-dts)."typesVersions" is supported in allmoduleResolution modes; however, the field is not read in situations whenpackage.json"exports" are read.

Example: redirect all requests to a subdirectory

Scenario: a module imports"pkg" using TypeScript 5.2, wherenode_modules/pkg/package.json is:

json
{
"name":"pkg",
"version":"1.0.0",
"types":"./index.d.ts",
"typesVersions": {
">=3.1": {
"*": ["ts3.1/*"]
}
}
}

Resolution process:

  1. (Depending on compiler options) Does"exports" exist?No.
  2. Does"typesVersions" exist?Yes.
  3. Is the TypeScript version>=3.1?Yes. Remember the mapping"*": ["ts3.1/*"].
  4. Are we resolving a subpath after the package name?No, just the root"pkg".
  5. Does"types" exist?Yes.
  6. Does any key in"typesVersions" match./index.d.ts?Yes,"*" matches and setsindex.d.ts to be the substitution.
  7. Ints3.1/*, replace* with the substitution./index.d.ts:ts3.1/index.d.ts.
  8. Does the path./ts3.1/index.d.ts have a recognized TypeScript file extension?Yes, so don’t use extension substitution.
  9. Return the path./ts3.1/index.d.ts if the file exists,undefined otherwise.
Example: redirect requests for a specific file

Scenario: a module imports"pkg" using TypeScript 3.9, wherenode_modules/pkg/package.json is:

json
{
"name":"pkg",
"version":"1.0.0",
"types":"./index.d.ts",
"typesVersions": {
"<4.0": {"index.d.ts": ["index.v3.d.ts"] }
}
}

Resolution process:

  1. (Depending on compiler options) Does"exports" exist?No.
  2. Does"typesVersions" exist?Yes.
  3. Is the TypeScript version<4.0?Yes. Remember the mapping"index.d.ts": ["index.v3.d.ts"].
  4. Are we resolving a subpath after the package name?No, just the root"pkg".
  5. Does"types" exist?Yes.
  6. Does any key in"typesVersions" match./index.d.ts?Yes,"index.d.ts" matches.
  7. Does the path./index.v3.d.ts have a recognized TypeScript file extension?Yes, so don’t use extension substitution.
  8. Return the path./index.v3.d.ts if the file exists,undefined otherwise.

package.json"main" and"types"

If a directory’spackage.json"exports" field is not read (either due to compiler options, or because it is not present, or because the directory is being resolved as adirectory module instead of anode_modules package) and the module specifier does not have a subpath after the package name or package.json-containing directory, TypeScript will attempt to resolve from these package.json fields, in order, in an attempt to find the main module for the package or directory:

  • "types"
  • "typings" (legacy)
  • "main"

The declaration file found at"types" is assumed to be an accurate representation of the implementation file found at"main". If"types" and"typings" are not present or cannot be resolved, TypeScript will read the"main" field and performextension substitution to find a declaration file.

When publishing a typed package to npm, it’s recommended to include a"types" field even ifextension substitution orpackage.json"exports" make it unnecessary, because npm shows a TS icon on the package registry listing only if the package.json contains a"types" field.

Package-relative file paths

If neitherpackage.json"exports" norpackage.json"typesVersions" apply, subpaths of a bare package specifier resolve relative to the package directory, according to applicablerelative path resolution rules. In modes that respect [package.json"exports"], this behavior is blocked by the mere presence of the"exports" field in the package’s package.json, even if the import fails to resolve through"exports", as demonstrated inan example above. On the other hand, if the import fails to resolve through"typesVersions", a package-relative file path resolution is attempted as a fallback.

When package-relative paths are supported, they resolve under the same rules as any other relative path considering themoduleResolution mode and context. For example, in--moduleResolution nodenext,directory modules andextensionless paths are only supported inrequire calls, not inimports:

ts
// @Filename: module.mts
import"pkg/dist/foo";// ❌ import, needs `.js` extension
import"pkg/dist/foo.js";// ✅
importfoo =require("pkg/dist/foo");// ✅ require, no extension needed

package.json"imports" and self-name imports

WhenmoduleResolution is set tonode16,nodenext, orbundler, andresolvePackageJsonImports is not disabled, TypeScript will attempt to resolve import paths beginning with# through the"imports" field of the nearest ancestor package.json of the importing file. Similarly, whenpackage.json"exports" lookups are enabled, TypeScript will attempt to resolve import paths beginning with the current package name—that is, the value in the"name" field of the nearest ancestor package.json of the importing file—through the"exports" field of that package.json. Both of these features allow files in a package to import other files in the same package, replacing a relative import path.

TypeScript follows Node.js’s resolution algorithm for"imports" andself references exactly up until a file path is resolved. At that point, TypeScript’s resolution algorithm forks based on whether the package.json containing the"imports" or"exports" being resolved belongs to anode_modules dependency or the local project being compiled (i.e., its directory contains the tsconfig.json file for the project that contains the importing file):

  • If the package.json is innode_modules, TypeScript will applyextension substitution to the file path if it doesn’t already have a recognized TypeScript file extension, and check for the existence of the resulting file paths.
  • If the package.json is part of the local project, an additional remapping step is performed in order to find theinput TypeScript implementation file that will eventually produce the output JavaScript or declaration file path that was resolved from"imports". Without this step, any compilation that resolves an"imports" path would be referencing output files from theprevious compilation instead of other input files that are intended to be included in the current compilation. This remapping uses theoutDir/declarationDir androotDir from the tsconfig.json, so using"imports" usually requires an explicitrootDir to be set.

This variation allows package authors to write"imports" and"exports" fields that reference only the compilation outputs that will be published to npm, while still allowing local development to use the original TypeScript source files.

Example: local project with conditions

Scenario:"/src/main.mts" imports"#utils" with conditions["types", "node", "import"] (determined bymoduleResolution setting and the context that triggered the module resolution request) in a project directory with a tsconfig.json and package.json:

json
// tsconfig.json
{
"compilerOptions": {
"moduleResolution":"node16",
"resolvePackageJsonImports":true,
"rootDir":"./src",
"outDir":"./dist"
}
}
json
// package.json
{
"name":"pkg",
"imports": {
"#utils": {
"import":"./dist/utils.d.mts",
"require":"./dist/utils.d.cts"
}
}
}

Resolution process:

  1. Import path starts with#, try to resolve through"imports".
  2. Does"imports" exist in the nearest ancestor package.json?Yes.
  3. Does"#utils" exist in the"imports" object?Yes.
  4. The value atimports["#utils"] is an object—it must be specifying conditions.
  5. Does the first condition"import" match this request?Yes.
  6. Should we attempt to map the output path to an input path?Yes, because:
    • Is the package.json innode_modules?No, it’s in the local project.
    • Is the tsconfig.json within the package.json directory?Yes.
  7. In./dist/utils.d.mts, replace theoutDir prefix withrootDir../src/utils.d.mts
  8. Replace the output extension.d.mts with the corresponding input extension.mts../src/utils.mts
  9. Return the path"./src/utils.mts" if the file exists.
  10. Otherwise, return the path"./dist/utils.d.mts" if the file exists.
Example:node_modules dependency with subpath pattern

Scenario:"/node_modules/pkg/main.mts" imports"#internal/utils" with conditions["types", "node", "import"] (determined bymoduleResolution setting and the context that triggered the module resolution request) with the package.json:

json
// /node_modules/pkg/package.json
{
"name":"pkg",
"imports": {
"#internal/*": {
"import":"./dist/internal/*.mjs",
"require":"./dist/internal/*.cjs"
}
}
}

Resolution process:

  1. Import path starts with#, try to resolve through"imports".
  2. Does"imports" exist in the nearest ancestor package.json?Yes.
  3. Does"#internal/utils" exist in the"imports" object?No, check for pattern matches.
  4. Does any key with a* match"#internal/utils"?Yes,"#internal/*" matches and setsutils to be the substitution.
  5. The value atimports["#internal/*"] is an object—it must be specifying conditions.
  6. Does the first condition"import" match this request?Yes.
  7. Should we attempt to map the output path to an input path?No, because the package.json is innode_modules.
  8. In./dist/internal/*.mjs, replace* with the substitutionutils../dist/internal/utils.mjs
  9. Does the path./dist/internal/utils.mjs have a recognized TypeScript file extension?No, try extension substitution.
  10. Viaextension substitution, try the following paths, returning the first one that exists, orundefined otherwise:
    1. ./dist/internal/utils.mts
    2. ./dist/internal/utils.d.mts
    3. ./dist/internal/utils.mjs

node16,nodenext

These modes reflect the module resolution behavior of Node.js v12 and later. (node16 andnodenext are currently identical, but if Node.js makes significant changes to its module system in the future,node16 will be frozen whilenodenext will be updated to reflect the new behavior.) In Node.js, the resolution algorithm for ECMAScript imports is significantly different from the algorithm for CommonJSrequire calls. For each module specifier being resolved, the syntax and themodule format of the importing file are first used to determine whether the module specifier will be in animport orrequire in the emitted JavaScript. That information is then passed into the module resolver to determine which resolution algorithm to use (and whether to use the"import" or"require" condition for package.json"exports" or"imports").

TypeScript files that aredetermined to be in CommonJS format may still useimport andexport syntax by default, but the emitted JavaScript will userequire andmodule.exports instead. This means that it’s common to seeimport statements that are resolved using therequire algorithm. If this causes confusion, theverbatimModuleSyntax compiler option can be enabled, which prohibits the use ofimport statements that would be emitted asrequire calls.

Note that dynamicimport() calls are always resolved using theimport algorithm, according to Node.js’s behavior. However,import() types are resolved according to the format of the importing file (for backward compatibility with existing CommonJS-format type declarations):

ts
// @Filename: module.mts
importxfrom"./mod.js";// `import` algorithm due to file format (emitted as-written)
import("./mod.js");// `import` algorithm due to syntax (emitted as-written)
typeMod =typeofimport("./mod.js");// `import` algorithm due to file format
importmod =require("./mod");// `require` algorithm due to syntax (emitted as `require`)
// @Filename: commonjs.cts
importxfrom"./mod";// `require` algorithm due to file format (emitted as `require`)
import("./mod.js");// `import` algorithm due to syntax (emitted as-written)
typeMod =typeofimport("./mod");// `require` algorithm due to file format
importmod =require("./mod");// `require` algorithm due to syntax (emitted as `require`)

Implied and enforced options

Supported features

Features are listed in order of precedence.

importrequire
paths
baseUrl
node_modules package lookups
package.json"exports"✅ matchestypes,node,import✅ matchestypes,node,require
package.json"imports" and self-name imports✅ matchestypes,node,import✅ matchestypes,node,require
package.json"typesVersions"
Package-relative paths✅ whenexports not present✅ whenexports not present
Full relative paths
Extensionless relative paths
Directory modules

bundler

--moduleResolution bundler attempts to model the module resolution behavior common to most JavaScript bundlers. In short, this means supporting all the behaviors traditionally associated with Node.js’s CommonJSrequire resolution algorithm likenode_modules lookups,directory modules, andextensionless paths, while also supporting newer Node.js resolution features likepackage.json"exports" andpackage.json"imports".

It’s instructive to think about the similarities and differences between--moduleResolution bundler and--moduleResolution nodenext, particularly in how they decide what conditions to use when resolving package.json"exports" or"imports". Consider an import statement in a.ts file:

ts
// index.ts
import {foo }from"pkg";

Recall that in--module nodenext --moduleResolution nodenext, the--module setting firstdetermines whether the import will be emitted to the.js file as animport orrequire call, then passes that information to TypeScript’s module resolver, which decides whether to match"import" or"require" conditions in"pkg"’s package.json"exports" accordingly. Let’s assume that there’s no package.json in scope of this file. The file extension is.ts, so the output file extension will be.js, which Node.js will interpret as CommonJS, so TypeScript will emit thisimport as arequire call. So, the module resolver will use therequire condition as it resolves"exports" from"pkg".

The same process happens in--moduleResolution bundler, but the rules for deciding whether to emit animport orrequire call for this import statement will be different, since--moduleResolution bundler necessitates using--module esnext or--module preserve. In both of those modes, ESMimport declarations always emit as ESMimport declarations, so TypeScript’s module resolver will receive that information and use the"import" condition as it resolves"exports" from"pkg".

This explanation may be somewhat unintuitive, since--moduleResolution bundler is usually used in combination with--noEmit—bundlers typically process raw.ts files and perform module resolution on untransformedimports orrequires. However, for consistency, TypeScript still uses the hypothetical emit decided bymodule to inform module resolution and type checking. This makes--module preserve the best choice whenever a runtime or bundler is operating on raw.ts files, since it implies no transformation. Under--module preserve --moduleResolution bundler, you can write imports and requires in the same file that will resolve with theimport andrequire conditions, respectively:

ts
// index.ts
importpkg1from"pkg";// Resolved with "import" condition
importpkg2 =require("pkg");// Resolved with "require" condition

Implied and enforced options

  • --moduleResolution bundler must be paired with--module esnext or--module preserve.
  • --moduleResolution bundler implies--allowSyntheticDefaultImports.

Supported features

node10 (formerly known asnode)

--moduleResolution node was renamed tonode10 (keepingnode as an alias for backward compatibility) in TypeScript 5.0. It reflects the CommonJS module resolution algorithm as it existed in Node.js versions earlier than v12. It should no longer be used.

Supported features

classic

Do not useclassic.

The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request

Contributors to this page:
ABAndrew Branch  (8)
YPYifan Pan  (1)
SFSean Flanigan  (1)
SFShane Fontaine  (1)
Zzhennann  (1)
1+

Last updated: Jul 14, 2025