Interop Constraints
allowSyntheticDefaultImports
Default:
trueif esModuleInterop is enabled, module issystem, or moduleResolution isbundler;falseotherwise.Released: 1.8
Related: esModuleInterop
When set to true, allowSyntheticDefaultImports allows you to write an import like:
import React from "react";instead of:
import * as React from "react";When the module does not explicitly specify a default export.
For example, without allowSyntheticDefaultImports as true:
// @filename: utilFunctions.js
const getStringLength = (str) => str.length;
module.exports = {
getStringLength,
};
// @filename: index.ts
import utils from "./utilFunctions";Module '"/home/runner/work/ts-website/ts-website/utilFunctions"' has no default export.
const count = utils.getStringLength("Check JS");TryThis code raises an error because there isn't a default object which you can import. Even though it feels like it should. For convenience, transpilers like Babel will automatically create a default if one isn't created. Making the module look a bit more like:
// @filename: utilFunctions.js
const getStringLength = (str) => str.length;
const allFunctions = {
getStringLength,
};
module.exports = allFunctions;
module.exports.default = allFunctions;This flag does not affect the JavaScript emitted by TypeScript, it's only for the type checking. This option brings the behavior of TypeScript in-line with Babel, where extra code is emitted to make using a default export of a module more ergonomic.
erasableSyntaxOnly
Node.js supports running TypeScript files directly as of v23.6; however, only TypeScript-specific syntax that does not have runtime semantics are supported under this mode. In other words, it must be possible to easily erase any TypeScript-specific syntax from a file, leaving behind a valid JavaScript file.
That means the following constructs are not supported:
enumdeclarationsnamespaces andmodules with runtime code- parameter properties in classes
- Non-ECMAScript
import =andexport =assignments <prefix>-style type assertions
// ❌ error: An `import ... = require(...)` alias
import foo = require("foo");
// ❌ error: A namespace with runtime code.
namespace container {
foo.method();
export type Bar = string;
}
// ❌ error: An `import =` alias
import Bar = container.Bar;
class Point {
// ❌ error: Parameter properties
constructor(public x: number, public y: number) { }
}
// ❌ error: An `export =` assignment.
export = Point;
// ❌ error: An enum declaration.
enum Direction {
Up,
Down,
Left,
Right,
}
// ❌ error: <prefix>-style type assertion.
const num = <number>1;Similar tools like ts-blank-space or Amaro (the underlying library for type-stripping in Node.js) have the same limitations. These tools will provide helpful error messages if they encounter code that doesn't meet these requirements, but you still won't find out your code doesn't work until you actually try to run it.
The --erasableSyntaxOnly flag will cause TypeScript to error on most TypeScript-specific constructs that have runtime behavior.
class C {
constructor(public x: number) { }
// ~~~~~~~~~~~~~~~~
// error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
}
}Typically, you will want to combine this flag with the --verbatimModuleSyntax, which ensures that a module contains the appropriate import syntax, and that import elision does not take place.
esModuleInterop Recommended
Default:
trueif module isnode16,nodenext, orpreserve;falseotherwise.Released: 2.7
Related: allowSyntheticDefaultImports
By default (with esModuleInterop false or not set) TypeScript treats CommonJS/AMD/UMD modules similar to ES6 modules. In doing this, there are two parts in particular which turned out to be flawed assumptions:
a namespace import like
import * as moment from "moment"acts the same asconst moment = require("moment")a default import like
import moment from "moment"acts the same asconst moment = require("moment").default
This mis-match causes these two issues:
the ES6 modules spec states that a namespace import (
import * as x) can only be an object, by having TypeScript treating it the same as= require("x")then TypeScript allowed for the import to be treated as a function and be callable. That's not valid according to the spec.while accurate to the ES6 modules spec, most libraries with CommonJS/AMD/UMD modules didn't conform as strictly as TypeScript's implementation.
Turning on esModuleInterop will fix both of these problems in the code transpiled by TypeScript. The first changes the behavior in the compiler, the second is fixed by two new helper functions which provide a shim to ensure compatibility in the emitted JavaScript:
import * as fs from "fs";
import _ from "lodash";
fs.readFileSync("file.txt", "utf8");
_.chunk(["a", "b", "c", "d"], 2);With esModuleInterop disabled:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const lodash_1 = require("lodash");
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);
TryWith esModuleInterop set to true:
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs = __importStar(require("fs"));
const lodash_1 = __importDefault(require("lodash"));
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);
TryNote: The namespace import import * as fs from "fs" only accounts for properties which are owned (basically properties set on the object and not via the prototype chain) on the imported object. If the module you're importing defines its API using inherited properties, you need to use the default import form (import fs from "fs"), or disable esModuleInterop.
Note: You can make JS emit terser by enabling importHelpers:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
fs.readFileSync("file.txt", "utf8");
lodash_1.default.chunk(["a", "b", "c", "d"], 2);
TryEnabling esModuleInterop will also enable allowSyntheticDefaultImports.
forceConsistentCasingInFileNames Recommended
Default:
trueReleased: 1.8
TypeScript follows the case sensitivity rules of the file system it's running on. This can be problematic if some developers are working in a case-sensitive file system and others aren't. If a file attempts to import fileManager.ts by specifying ./FileManager.ts the file will be found in a case-insensitive file system, but not on a case-sensitive file system.
When this option is set, TypeScript will issue an error if a program tries to include a file by a casing different from the casing on disk.
isolatedDeclarations
- Released: 5.5
Require sufficient annotation on exports so other tools can trivially generate declaration files.
For more information, see the 5.5 release notes
isolatedModules
Default:
trueif verbatimModuleSyntax;falseotherwise.Released: 1.5
While you can use TypeScript to produce JavaScript code from TypeScript code, it's also common to use other transpilers such as Babel to do this. However, other transpilers only operate on a single file at a time, which means they can't apply code transforms that depend on understanding the full type system. This restriction also applies to TypeScript's ts.transpileModule API which is used by some build tools.
These limitations can cause runtime problems with some TypeScript features like const enums and namespaces. Setting the isolatedModules flag tells TypeScript to warn you if you write certain code that can't be correctly interpreted by a single-file transpilation process.
It does not change the behavior of your code, or otherwise change the behavior of TypeScript's checking and emitting process.
Some examples of code which does not work when isolatedModules is enabled.
Exports of Non-Value Identifiers
In TypeScript, you can import a type and then subsequently export it:
import { someType, someFunction } from "someModule";
someFunction();
export { someType, someFunction };TryBecause there's no value for someType, the emitted export will not try to export it (this would be a runtime error in JavaScript):
export { someFunction };Single-file transpilers don't know whether someType produces a value or not, so it's an error to export a name that only refers to a type.
Non-Module Files
If isolatedModules is set, namespaces are only allowed in modules (which means it has some form of import/export). An error occurs if a namespace is found in a non-module file:
namespace Instantiated {Namespaces are not allowed in global script files when 'isolatedModules' is enabled. If this file is not intended to be a global script, set 'moduleDetection' to 'force' or add an empty 'export {}' statement. export const x = 1;
}TryThis restriction doesn't apply to .d.ts files.
References to const enum members
In TypeScript, when you reference a const enum member, the reference is replaced by its actual value in the emitted JavaScript. Changing this TypeScript:
To this JavaScript:
Without knowledge of the values of these members, other transpilers can't replace the references to Numbers, which would be a runtime error if left alone (since there are no Numbers object at runtime). Because of this, when isolatedModules is set, it is an error to reference an ambient const enum member.
preserveSymlinks
- Released: 2.5
This is to reflect the same flag in Node.js; which does not resolve the real path of symlinks.
This flag also exhibits the opposite behavior to Webpack’s resolve.symlinks option (i.e. setting TypeScript’s preserveSymlinks to true parallels setting Webpack’s resolve.symlinks to false, and vice-versa).
With this enabled, references to modules and packages (e.g. imports and /// <reference type="..." /> directives) are all resolved relative to the location of the symbolic link file, rather than relative to the path that the symbolic link resolves to.
verbatimModuleSyntax
- Released: 5.0
By default, TypeScript does something called import elision. Basically, if you write something like
import { Car } from "./car";
export function drive(car: Car) {
// ...
}TypeScript detects that you're only using an import for types and drops the import entirely. Your output JavaScript might look something like this:
export function drive(car) {
// ...
}Most of the time this is good, because if Car isn't a value that's exported from ./car, we'll get a runtime error.
But it does add a layer of complexity for certain edge cases. For example, notice there's no statement like import "./car"; - the import was dropped entirely. That actually makes a difference for modules that have side-effects or not.
TypeScript's emit strategy for JavaScript also has another few layers of complexity - import elision isn't always just driven by how an import is used - it often consults how a value is declared as well. So it's not always clear whether code like the following
export { Car } from "./car";should be preserved or dropped. If Car is declared with something like a class, then it can be preserved in the resulting JavaScript file. But if Car is only declared as a type alias or interface, then the JavaScript file shouldn't export Car at all.
While TypeScript might be able to make these emit decisions based on information from across files, not every compiler can.
The type modifier on imports and exports helps with these situations a bit. We can make it explicit whether an import or export is only being used for type analysis, and can be dropped entirely in JavaScript files by using the type modifier.
// This statement can be dropped entirely in JS output
import type * as car from "./car";
// The named import/export 'Car' can be dropped in JS output
import { type Car } from "./car";
export { type Car } from "./car";type modifiers are not quite useful on their own - by default, module elision will still drop imports, and nothing forces you to make the distinction between type and plain imports and exports. So TypeScript has the flag --importsNotUsedAsValues to make sure you use the type modifier, --preserveValueImports to prevent some module elision behavior, and --isolatedModules to make sure that your TypeScript code works across different compilers. Unfortunately, understanding the fine details of those 3 flags is hard, and there are still some edge cases with unexpected behavior.
TypeScript 5.0 introduces a new option called --verbatimModuleSyntax to simplify the situation. The rules are much simpler - any imports or exports without a type modifier are left around. Anything that uses the type modifier is dropped entirely.
// Erased away entirely.
import type { A } from "a";
// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";
// Rewritten to 'import {} from "xyz";'
import { type xyz } from "xyz";With this new option, what you see is what you get.
That does have some implications when it comes to module interop though. Under this flag, ECMAScript imports and exports won't be rewritten to require calls when your settings or file extension implied a different module system. Instead, you'll get an error. If you need to emit code that uses require and module.exports, you'll have to use TypeScript's module syntax that predates ES2015:
| Input TypeScript | Output JavaScript |
|---|---|
ts | js |
ts | js |
While this is a limitation, it does help make some issues more obvious. For example, it's very common to forget to set the type field in package.json under --module node16. As a result, developers would start writing CommonJS modules instead of ES modules without realizing it, giving surprising lookup rules and JavaScript output. This new flag ensures that you're intentional about the file type you're using because the syntax is intentionally different.
Because --verbatimModuleSyntax provides a more consistent story than --importsNotUsedAsValues and --preserveValueImports, those two existing flags are being deprecated in its favor.
For more details, read up on the original pull request and its proposal issue.