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 declarationsexporttypeSomeType =/* ... */;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.tsimport {f,typeSomeInterface }from"./module.js";importtype {SomeType }from"./module.js";classCimplementsSomeInterface {constructor(p:SomeType) {f();}}exporttype {C };// @Filename: main.jsimport {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.tsimportfs =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.tsinterfaceOptions {/* ... */ }module.exports =Options;// Error: 'Options' only refers to a type, but is being used as a value here.export =Options;// Ok// @Filename: b.tsconstOptions =require("./a");constoptions:Options = {/* ... */ };// Error: 'Options' refers to a value, but is being used as a type here.// @Filename: c.tsimportOptions =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 that
node16
—nodenext
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:
target | moduleResolution | import assertions | import attributes | JSON imports | require(esm) | |
---|---|---|---|---|---|---|
node16 | es2022 | node16 | ❌ | ❌ | no restrictions | ❌ |
node18 | es2022 | node16 | ✅ | ✅ | needstype "json" | ❌ |
nodenext | esnext | nodenext | ❌ | ✅ | needstype "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:
- The
module.exports
of the CommonJS module is available as a default import to the ES module. - Properties (other than
default
) 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.
- The
- When a CommonJS module references an ES module:
- In
node16
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. - In
nodenext
, 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 dynamic
import()
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).
- In
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.tsimportx =require("mod");
js
// @Filename: main.jsimport {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.tsimportfsfrom"fs";// transformedconstdynamic =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"));// transformedconstdynamic =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 transforms
import x = require("...")
to arequire
call constructed from acreateRequire
import. - CommonJS emit leaves dynamic
import()
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’s
module
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.tsimportx, {y,z }from"mod";importmod =require("mod");constdynamic =import("mod");exportconste1 =0;exportdefault"default export";
js
// @Filename: main.jsimportx, {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
- Use
esnext
with--moduleResolution bundler
for bundlers, Bun, and tsx. - Do not use for Node.js. Use
node16
,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.tsimportx, {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.jsimportx, {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. Use
node16
,node18
, ornodenext
to emit CommonJS modules for Node.js. - Emitted files are CommonJS modules, but dependencies may be any format.
- Dynamic
import()
is transformed to a Promise of arequire()
call. esModuleInterop
affects the output code for default and namespace imports.
Examples
Output is shown with
esModuleInterop: false
.
ts
// @Filename: main.tsimportx, {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.tsimportmod =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
- Designed for use with theSystemJS module loader.
Examples
ts
// @Filename: main.tsimportx, {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.jsSystem.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.
- Supports
outFile
.
Examples
ts
// @Filename: main.tsimportx, {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.jsdefine(["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.tsimportx, {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 lookup | TypeScript lookup #1 | TypeScript lookup #2 | TypeScript lookup #3 | TypeScript lookup #4 | TypeScript 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.tsexport {};// @Filename: b.tsimport {}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.tsexport {};// @Filename: b.tsimport {}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.tsexport {};// @Filename: b.tsimport {}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` entryimport {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):
- For each ancestor directory of the importing file, if a
node_modules
directory exists within it:- If a directory with the same name as the package exists within
node_modules
:- Attempt to resolve types from the package directory.
- If a result is found, return it and stop the search.
- If a directory with the same name as the package exists within
node_modules/@types
:- Attempt to resolve types from the
@types
package directory. - If a result is found, return it and stop the search.
- Attempt to resolve types from the
- If a directory with the same name as the package exists within
- Repeat the previous search through all
node_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:
- Does
"exports"
exist?Yes. - Does
"exports"
have a"./subpath"
entry?Yes. - The value at
exports["./subpath"]
is an object—it must be specifying conditions. - Does the first condition
"import"
match this request?No. - Does the second condition
"require"
match this request?Yes. - Does the path
"./subpath/index.cjs"
have a recognized TypeScript file extension?No, so use extension substitution. - Viaextension substitution, try the following paths, returning the first one that exists, or
undefined
otherwise:./subpath/index.cts
./subpath/index.d.cts
./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:
- Does
"exports"
exist?Yes. - Does
"exports"
have a"./subpath"
entry?Yes. - The value at
exports["./subpath"]
is an object—it must be specifying conditions. - Does the first condition
"import"
match this request?Yes. - The value at
exports["./subpath"].import
is an object—it must be specifying conditions. - Does the first condition
"types"
match this request?Yes. - Does the path
"./types/subpath/index.d.mts"
have a recognized TypeScript file extension?Yes, so don’t use extension substitution. - 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:
- Does
"exports"
exist?Yes. - Does
"exports"
have a"./subpath"
entry?Yes. - The value at
exports["./subpath"]
is an object—it must be specifying conditions. - Does the first condition
"types@>=5.2"
match this request?No, 4.7.5 is not greater than or equal to 5.2. - Does the second condition
"types@>=4.6"
match this request?Yes, 4.7.5 is greater than or equal to 4.6. - Does the path
"./ts4.6/subpath/index.d.ts"
have a recognized TypeScript file extension?Yes, so don’t use extension substitution. - 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:
- Does
"exports"
exist?Yes. - Does
"exports"
have a"./wildcard.js"
entry?No. - Does any key with a
*
in it match"./wildcard.js"
?Yes,"./*.js"
matches and setswildcard
to be the substitution. - The value at
exports["./*.js"]
is an object—it must be specifying conditions. - Does the first condition
"types"
match this request?Yes. - In
./types/*.d.ts
, replace*
with the substitutionwildcard
../types/wildcard.d.ts
- Does the path
"./types/wildcard.d.ts"
have a recognized TypeScript file extension?Yes, so don’t use extension substitution. - 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:
- Does
"exports"
exist?Yes. - The value at
exports
is a string—it must be a file path for the package root ("."
). - Is the request
"pkg/dist/index.js"
for the package root?No, it has a subpathdist/index.js
. - Resolution fails; return
undefined
.
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:
- (Depending on compiler options) Does
"exports"
exist?No. - Does
"typesVersions"
exist?Yes. - Is the TypeScript version
>=3.1
?Yes. Remember the mapping"*": ["ts3.1/*"]
. - Are we resolving a subpath after the package name?No, just the root
"pkg"
. - Does
"types"
exist?Yes. - Does any key in
"typesVersions"
match./index.d.ts
?Yes,"*"
matches and setsindex.d.ts
to be the substitution. - In
ts3.1/*
, replace*
with the substitution./index.d.ts
:ts3.1/index.d.ts
. - Does the path
./ts3.1/index.d.ts
have a recognized TypeScript file extension?Yes, so don’t use extension substitution. - 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:
- (Depending on compiler options) Does
"exports"
exist?No. - Does
"typesVersions"
exist?Yes. - Is the TypeScript version
<4.0
?Yes. Remember the mapping"index.d.ts": ["index.v3.d.ts"]
. - Are we resolving a subpath after the package name?No, just the root
"pkg"
. - Does
"types"
exist?Yes. - Does any key in
"typesVersions"
match./index.d.ts
?Yes,"index.d.ts"
matches. - Does the path
./index.v3.d.ts
have a recognized TypeScript file extension?Yes, so don’t use extension substitution. - 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 inimport
s:
ts
// @Filename: module.mtsimport"pkg/dist/foo";// ❌ import, needs `.js` extensionimport"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 in
node_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:
- Import path starts with
#
, try to resolve through"imports"
. - Does
"imports"
exist in the nearest ancestor package.json?Yes. - Does
"#utils"
exist in the"imports"
object?Yes. - The value at
imports["#utils"]
is an object—it must be specifying conditions. - Does the first condition
"import"
match this request?Yes. - Should we attempt to map the output path to an input path?Yes, because:
- Is the package.json in
node_modules
?No, it’s in the local project. - Is the tsconfig.json within the package.json directory?Yes.
- Is the package.json in
- In
./dist/utils.d.mts
, replace theoutDir
prefix withrootDir
../src/utils.d.mts
- Replace the output extension
.d.mts
with the corresponding input extension.mts
../src/utils.mts
- Return the path
"./src/utils.mts"
if the file exists. - 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:
- Import path starts with
#
, try to resolve through"imports"
. - Does
"imports"
exist in the nearest ancestor package.json?Yes. - Does
"#internal/utils"
exist in the"imports"
object?No, check for pattern matches. - Does any key with a
*
match"#internal/utils"
?Yes,"#internal/*"
matches and setsutils
to be the substitution. - The value at
imports["#internal/*"]
is an object—it must be specifying conditions. - Does the first condition
"import"
match this request?Yes. - Should we attempt to map the output path to an input path?No, because the package.json is in
node_modules
. - In
./dist/internal/*.mjs
, replace*
with the substitutionutils
../dist/internal/utils.mjs
- Does the path
./dist/internal/utils.mjs
have a recognized TypeScript file extension?No, try extension substitution. - Viaextension substitution, try the following paths, returning the first one that exists, or
undefined
otherwise:./dist/internal/utils.mts
./dist/internal/utils.d.mts
./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 use
import
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.mtsimportxfrom"./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 formatimportmod =require("./mod");// `require` algorithm due to syntax (emitted as `require`)// @Filename: commonjs.ctsimportxfrom"./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 formatimportmod =require("./mod");// `require` algorithm due to syntax (emitted as `require`)
Implied and enforced options
--moduleResolution node16
andnodenext
must be paired with--module node16
,node18
, ornodenext
.
Supported features
Features are listed in order of precedence.
import | require | |
---|---|---|
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.tsimport {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 untransformedimport
s orrequire
s. 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.tsimportpkg1from"pkg";// Resolved with "import" conditionimportpkg2 =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
paths
✅baseUrl
✅node_modules
package lookups ✅- package.json
"exports"
✅ matchestypes
,import
/require
depending on syntax - package.json
"imports"
and self-name imports ✅ matchestypes
,import
/require
depending on syntax - package.json
"typesVersions"
✅ - Package-relative paths ✅ when
exports
not present - Full relative paths ✅
- Extensionless relative paths ✅
- Directory modules ✅
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
paths
✅baseUrl
✅node_modules
package lookups ✅- package.json
"exports"
❌ - package.json
"imports"
and self-name imports ❌ - package.json
"typesVersions"
✅ - Package-relative paths ✅
- Full relative paths ✅
- Extensionless relative paths ✅
- Directory modules ✅
classic
Do not useclassic
.
The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request ❤
Last updated: Jul 14, 2025