Skip to content
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here
Your logo here

Modules

allowArbitraryExtensions

In TypeScript 5.0, when an import path ends in an extension that isn't a known JavaScript or TypeScript file extension, the compiler will look for a declaration file for that path in the form of {file basename}.d.{extension}.ts. For example, if you are using a CSS loader in a bundler project, you might want to write (or generate) declaration files for those stylesheets:

css
/* app.css */
.cookie-banner {
  display: none;
}
ts
// app.d.css.ts
declare const css: {
  cookieBanner: string;
};
export default css;
ts
// App.tsx
import styles from "./app.css";

styles.cookieBanner; // string

By default, this import will raise an error to let you know that TypeScript doesn't understand this file type and your runtime might not support importing it. But if you've configured your runtime or bundler to handle it, you can suppress the error with the new --allowArbitraryExtensions compiler option.

Note that historically, a similar effect has often been achievable by adding a declaration file named app.css.d.ts instead of app.d.css.ts - however, this just worked through Node's require resolution rules for CommonJS. Strictly speaking, the former is interpreted as a declaration file for a JavaScript file named app.css.js. Because relative files imports need to include extensions in Node's ESM support, TypeScript would error on our example in an ESM file under --moduleResolution node16 or nodenext.

For more information, read up the proposal for this feature and its corresponding pull request.

allowImportingTsExtensions

--allowImportingTsExtensions allows TypeScript files to import each other with a TypeScript-specific extension like .ts, .mts, or .tsx.

This flag is only allowed when --noEmit or --emitDeclarationOnly is enabled, since these import paths would not be resolvable at runtime in JavaScript output files. The expectation here is that your resolver (e.g. your bundler, a runtime, or some other tool) is going to make these imports between .ts files work.

allowUmdGlobalAccess

When set to true, allowUmdGlobalAccess lets you access UMD exports as globals from inside module files. A module file is a file that has imports and/or exports. Without this flag, using an export from a UMD module requires an import declaration.

An example use case for this flag would be a web project where you know the particular library (like jQuery or Lodash) will always be available at runtime, but you can’t access it with an import.

baseUrl

Sets a base directory from which to resolve bare specifier module names. For example, in the directory structure:

project
├── ex.ts
├── hello
│   └── world.ts
└── tsconfig.json

With "baseUrl": "./", TypeScript will look for files starting at the same folder as the tsconfig.json:

ts
import { helloWorld } from "hello/world";

console.log(helloWorld);

This resolution has higher priority than lookups from node_modules.

This feature was designed for use in conjunction with AMD module loaders in the browser, and is not recommended in any other context. As of TypeScript 4.1, baseUrl is no longer required to be set when using paths.

customConditions

--customConditions takes a list of additional conditions that should succeed when TypeScript resolves from an exports or imports field of a package.json. These conditions are added to whatever existing conditions a resolver will use by default.

For example, when this field is set in a tsconfig.json as so:

jsonc
{
    "compilerOptions": {
        "target": "es2022",
        "moduleResolution": "bundler",
        "customConditions": ["my-condition"]
    }
}

Any time an exports or imports field is referenced in package.json, TypeScript will consider conditions called my-condition.

So when importing from a package with the following package.json

jsonc
{
    // ...
    "exports": {
        ".": {
            "my-condition": "./foo.mjs",
            "node": "./bar.mjs",
            "import": "./baz.mjs",
            "require": "./biz.mjs"
        }
    }
}

TypeScript will try to look for files corresponding to foo.mjs.

This field is only valid under the node16, nodenext, and bundler options for --moduleResolution.

module

Sets the module system for the program. See the theory behind TypeScript’s module option and its reference page for more information. You very likely want "nodenext" for modern Node.js projects and preserve or esnext for code that will be bundled.

Changing module affects moduleResolution which also has a reference page.

Here's some example output for this file:

ts
// @filename: index.ts
import { 
valueOfPi
} from "./constants";
export const
twoPi
=
valueOfPi
* 2;
Try

CommonJS

ts
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_1 = require("./constants");
exports.twoPi = constants_1.valueOfPi * 2;
Try

UMD

ts
(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./constants"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.twoPi = void 0;
    const constants_1 = require("./constants");
    exports.twoPi = constants_1.valueOfPi * 2;
});
Try

AMD

ts
define(["require", "exports", "./constants"], function (require, exports, constants_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    exports.twoPi = void 0;
    exports.twoPi = constants_1.valueOfPi * 2;
});
Try

System

ts
System.register(["./constants"], function (exports_1, context_1) {
    "use strict";
    var constants_1, twoPi;
    var __moduleName = context_1 && context_1.id;
    return {
        setters: [
            function (constants_1_1) {
                constants_1 = constants_1_1;
            }
        ],
        execute: function () {
            exports_1("twoPi", twoPi = constants_1.valueOfPi * 2);
        }
    };
});
Try

ESNext

ts
import { valueOfPi } from "./constants";
export const twoPi = valueOfPi * 2;
Try

ES2015/ES6/ES2020/ES2022

ts
import { valueOfPi } from "./constants";
export const twoPi = valueOfPi * 2;
Try

In addition to the base functionality of ES2015/ES6, ES2020 adds support for dynamic imports, and import.meta while ES2022 further adds support for top level await.

node16/node18/node20/nodenext

The node16, node18, node20, and nodenext modes integrate with Node's native ECMAScript Module support. The emitted JavaScript uses either CommonJS or ES2020 output depending on the file extension and the value of the type setting in the nearest package.json. Module resolution also works differently. You can learn more in the handbook and Modules Reference.

  • node16 is available from TypeScript 4.7
  • node18 is available from TypeScript 5.8 as a replacement for node16, with added support for import attributes.
  • node20 adds support for require(ESM).
  • nodenext is available from TypeScript 4.7, but its behavior changes with the latest stable versions of Node.js. --module nodenext implies the floating --target esnext.

preserve

In --module preserve (added in TypeScript 5.4), ECMAScript imports and exports written in input files are preserved in the output, and CommonJS-style import x = require("...") and export = ... statements are emitted as CommonJS require and module.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).

ts
import { valueOfPi } from "./constants";
const constants = require("./constants");
export const piSquared = valueOfPi * constants.valueOfPi;
Try

While it’s rare to need to mix imports and require calls in the same file, this module 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 setting noEmit? TypeScript’s type checking and module resolution behavior are affected by the module format that it would emit. Setting module 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.

None

ts
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const constants_1 = require("./constants");
exports.twoPi = constants_1.valueOfPi * 2;
Try

moduleResolution

Specify the module resolution strategy:

  • 'node16' or 'nodenext' for modern versions of Node.js. Node.js v12 and later supports both ECMAScript imports and CommonJS require, which resolve using different algorithms. These moduleResolution values, when combined with the corresponding module values, picks the right algorithm for each resolution based on whether Node.js will see an import or require in the output JavaScript code.
  • 'node10' (previously called 'node') for Node.js versions older than v10, which only support CommonJS require. You probably won't need to use node10 in modern code.
  • 'bundler' for use with bundlers. Like node16 and nodenext, this mode supports package.json "imports" and "exports", but unlike the Node.js resolution modes, bundler never requires file extensions on relative paths in imports.
  • 'classic' was used in TypeScript before the release of 1.6. classic should not be used.

There are reference pages explaining the theory behind TypeScript’s module resolution and the details of each option.

moduleSuffixes

Provides a way to override the default list of file name suffixes to search when resolving a module.

json
{
    "compilerOptions": {
        "moduleSuffixes": [".ios", ".native", ""]
    }
}

Given the above configuration, an import like the following:

ts
import * as foo from "./foo";

TypeScript will look for the relative files ./foo.ios.ts, ./foo.native.ts, and finally ./foo.ts.

Note the empty string "" in moduleSuffixes which is necessary for TypeScript to also look-up ./foo.ts.

This feature can be useful for React Native projects where each target platform can use a separate tsconfig.json with differing moduleSuffixes.

noResolve

By default, TypeScript will examine the initial set of files for import and <reference directives and add these resolved files to your program.

If noResolve is set, this process doesn't happen. However, import statements are still checked to see if they resolve to a valid module, so you'll need to make sure this is satisfied by some other means.

noUncheckedSideEffectImports

  • Default: true

  • Released: 5.6

In JavaScript it's possible to import a module without actually importing any values from it.

ts
import "some-module";

These imports are often called side effect imports because the only useful behavior they can provide is by executing some side effect (like registering a global variable, or adding a polyfill to a prototype).

By default, TypeScript will not check these imports for validity. If the import resolves to a valid source file, TypeScript will load and check the file. If no source file is found, TypeScript will silently ignore the import.

This is surprising behavior, but it partially stems from modeling patterns in the JavaScript ecosystem. For example, this syntax has also been used with special loaders in bundlers to load CSS or other assets. Your bundler might be configured in such a way where you can include specific .css files by writing something like the following:

tsx
import "./button-component.css";

export function Button() {
    // ...
}

Still, this masks potential typos on side effect imports.

When --noUncheckedSideEffectImports is enabled, TypeScript will error if it can't find a source file for a side effect import.

ts
import "oops-this-module-does-not-exist";
//     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// error: Cannot find module 'oops-this-module-does-not-exist' or its corresponding
//        type declarations.

When enabling this option, some working code may now receive an error, like in the CSS example above. To work around this, users who want to just write side effect imports for assets might be better served by writing what's called an ambient module declaration with a wildcard specifier. It would go in a global file and look something like the following:

ts
// ./src/globals.d.ts

// Recognize all CSS files as module imports.
declare module "*.css" {}

In fact, you might already have a file like this in your project! For example, running something like vite init might create a similar vite-env.d.ts.

paths

A series of entries which re-map imports to lookup locations relative to the baseUrl if set, or to the tsconfig file itself otherwise. There is a larger coverage of paths in the moduleResolution reference page.

paths lets you declare how TypeScript should resolve an import in your require/imports.

json
{
  "compilerOptions": {
    "paths": {
      "jquery": ["./vendor/jquery/dist/jquery"]
    }
  }
}

This would allow you to be able to write import "jquery", and get all of the correct typing locally.

json
{
  "compilerOptions": {
    "paths": {
        "app/*": ["./src/app/*"],
        "config/*": ["./src/app/_config/*"],
        "environment/*": ["./src/environments/*"],
        "shared/*": ["./src/app/_shared/*"],
        "helpers/*": ["./src/helpers/*"],
        "tests/*": ["./src/tests/*"]
    }
  }
}

In this case, you can tell the TypeScript file resolver to support a number of custom prefixes to find code.

Note that this feature does not change how import paths are emitted by tsc, so paths should only be used to inform TypeScript that another tool has this mapping and will use it at runtime or when bundling.

resolveJsonModule

Allows importing modules with a .json extension, which is a common practice in node projects. This includes generating a type for the import based on the static JSON shape.

TypeScript does not support resolving JSON files by default:

ts
// @filename: settings.json
{
    "repo": "TypeScript",
    "dry": false,
    "debug": false
}
// @filename: index.ts
import 
settings
from "./settings.json";
Cannot find module './settings.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.
settings
.debug === true;
settings
.dry === 2;
Try

Enabling the option allows importing JSON, and validating the types in that JSON file.

ts
// @filename: settings.json
{
    "repo": "TypeScript",
    "dry": false,
    "debug": false
}
// @filename: index.ts
import 
settings
from "./settings.json";
settings
.
debug
=== true;
settings.dry === 2;
This comparison appears to be unintentional because the types 'boolean' and 'number' have no overlap.
Try

resolvePackageJsonExports

--resolvePackageJsonExports forces TypeScript to consult the exports field of package.json files if it ever reads from a package in node_modules.

This option defaults to true under the node16, nodenext, and bundler options for --moduleResolution.

resolvePackageJsonImports

--resolvePackageJsonImports forces TypeScript to consult the imports field of package.json files when performing a lookup that starts with # from a file whose ancestor directory contains a package.json.

This option defaults to true under the node16, nodenext, and bundler options for --moduleResolution.

rewriteRelativeImportExtensions

Rewrite .ts, .tsx, .mts, and .cts file extensions in relative import paths to their JavaScript equivalent in output files.

For more information, see the TypeScript 5.7 release notes.

rootDir

  • Default: Computed from the list of input files.

  • Released: 1.5

Default: The longest common path of all non-declaration input files. If composite is set, the default is instead the directory containing the tsconfig.json file.

When TypeScript compiles files, it keeps the same directory structure in the output directory as exists in the input directory.

For example, let's say you have some input files:

MyProj
├── tsconfig.json
├── core
│   ├── a.ts
│   ├── b.ts
│   ├── sub
│   │   ├── c.ts
├── types.d.ts

The inferred value for rootDir is the longest common path of all non-declaration input files, which in this case is core/.

If your outDir was dist, TypeScript would write this tree:

MyProj
├── dist
│   ├── a.js
│   ├── b.js
│   ├── sub
│   │   ├── c.js

However, you may have intended for core to be part of the output directory structure. By setting rootDir: "." in tsconfig.json, TypeScript would write this tree:

MyProj
├── dist
│   ├── core
│   │   ├── a.js
│   │   ├── b.js
│   │   ├── sub
│   │   │   ├── c.js

Importantly, rootDir does not affect which files become part of the compilation. It has no interaction with the include, exclude, or files tsconfig.json settings.

Note that TypeScript will never write an output file to a directory outside of outDir, and will never skip emitting a file. For this reason, rootDir also enforces that all files which need to be emitted are underneath the rootDir path.

For example, let's say you had this tree:

MyProj
├── tsconfig.json
├── core
│   ├── a.ts
│   ├── b.ts
├── helpers.ts

It would be an error to specify rootDir as core and include as * because it creates a file (helpers.ts) that would need to be emitted outside the outDir (i.e. ../helpers.js).

rootDirs

  • Default: Computed from the list of input files.

  • Released: 2.0

Using rootDirs, you can inform the compiler that there are many "virtual" directories acting as a single root. This allows the compiler to resolve relative module imports within these "virtual" directories, as if they were merged in to one directory.

For example:

 src
 └── views
     └── view1.ts (can import "./template1", "./view2`)
     └── view2.ts (can import "./template1", "./view1`)

 generated
 └── templates
         └── views
             └── template1.ts (can import "./view1", "./view2")
json
{
  "compilerOptions": {
    "rootDirs": ["src/views", "generated/templates/views"]
  }
}

This does not affect how TypeScript emits JavaScript, it only emulates the assumption that they will be able to work via those relative paths at runtime.

rootDirs can be used to provide a separate "type layer" to files that are not TypeScript or JavaScript by providing a home for generated .d.ts files in another folder. This technique is useful for bundled applications where you use import of files that aren't necessarily code:

sh
 src
 └── index.ts
 └── css
     └── main.css
     └── navigation.css

 generated
 └── css
     └── main.css.d.ts
     └── navigation.css.d.ts
json
{
  "compilerOptions": {
    "rootDirs": ["src", "generated"]
  }
}

This technique lets you generate types ahead of time for the non-code source files. Imports then work naturally based off the source file's location. For example ./src/index.ts can import the file ./src/css/main.css and TypeScript will be aware of the bundler's behavior for that filetype via the corresponding generated declaration file.

ts
// @filename: index.ts
import { 
appClass
} from "./main.css";
Try

typeRoots

By default all visible "@types" packages are included in your compilation. Packages in node_modules/@types of any enclosing folder are considered visible. For example, that means packages within ./node_modules/@types/, ../node_modules/@types/, ../../node_modules/@types/, and so on.

If typeRoots is specified, only packages under typeRoots will be included. For example:

json
{
  "compilerOptions": {
    "typeRoots": ["./typings", "./vendor/types"]
  }
}

This config file will include all packages under ./typings and ./vendor/types, and no packages from ./node_modules/@types. All paths are relative to the tsconfig.json.

types

By default all visible "@types" packages are included in your compilation. Packages in node_modules/@types of any enclosing folder are considered visible. For example, that means packages within ./node_modules/@types/, ../node_modules/@types/, ../../node_modules/@types/, and so on.

If types is specified, only packages listed will be included in the global scope. For instance:

json
{
  "compilerOptions": {
    "types": ["node", "jest", "express"]
  }
}

This tsconfig.json file will only include ./node_modules/@types/node, ./node_modules/@types/jest and ./node_modules/@types/express. Other packages under node_modules/@types/* will not be included.

What does this affect?

This option does not affect how @types/* are included in your application code, for example if you had the above compilerOptions example with code like:

ts
import * as moment from "moment";

moment().format("MMMM Do YYYY, h:mm:ss a");

The moment import would be fully typed.

When you have this option set, by not including a module in the types array it:

  • Will not add globals to your project (e.g process in node, or expect in Jest)
  • Will not have exports appear as auto-import recommendations

This feature differs from typeRoots in that it is about specifying only the exact types you want included, whereas typeRoots supports saying you want particular folders.