• Home
  • About
  • KR

[All About tsconfig] Compiler options / Emit


[All About tsconfig] Compiler options / Emit

In this post, following the previous [All About tsconfig] Compiler options / Modules post, I’ll introduce options that control output files among tsconfig’s compilation options.

These options determine the appearance of JavaScript code generated after compiling code written in TypeScript, such as how to express syntax supported only in TypeScript in JavaScript, or how to express it when transpiling ES6 or higher syntax to ES5.

Of course, since there are parts that overlap with features provided by transpilers like Babel, if using TSC and Babel together, there may be cases where you don’t need to meticulously set all options in tsconfig.

declaration

Value Description
true Generate type declaration files together when compiling
false (default) Generate only JavaScript files when compiling

The declaration option is an option that decides whether to output TypeScript’s *.d.ts files or not. Since this option is off by default, if you compile TypeScript without separate settings, you can see only *.js files generated alone.

// math.ts
export const add = (x: number) => (y: number) => x + y;
// math.js
export var add = function (x) { return function (y) { return x + y; }; };

But if you turn on the declaration option, in addition to the generated JavaScript file, it also generates *.d.ts files containing type declarations.

// math.d.ts
export declare const add: (x: number) => (y: number) => number;

If you want modules you made to support TypeScript, you must allow users to directly get the module’s source code or provide JavaScript files but also provide type declaration files *.d.ts, so if developing TypeScript-targeted libraries, I recommend using this option to generate type declaration files together during compilation.

declarationDir

Type Description
string Path to directory to output type declaration files

The declarationDir option, as its name suggests, is an option to set the path to output type declaration files. At this time, the current path that . means refers to where the tsconfig file is located, so if you want to output type declaration files inside the directory where compiled files are located, you must directly specify directories like ./{outDir}/types rather than paths like ./types.

If you don’t separately set the declarationDir option, type declaration files are created in the same location as their original JavaScript files.

When declarationDir option is not set

myProject
├── math.ts
├── dist
│   ├── math.d.ts <
│   └── math.js
└── tsconfig.json
When declarationDir option is set to "./dist/types"

myProject
├── math.ts
├── dist
│   ├── math.js
│   └── types
│       └── math.d.ts <
└── tsconfig.json

Gathering type declaration files in one directory like this lets you conveniently specify the location of that package’s type declaration files later using package.json’s types property, so I tend to use the declarationDir option to gather type declarations in one place.

declarationMap

Value Description
true Generate mapping files connecting type declarations with source code
false (default) Don’t generate mapping files

The declarationMap option decides whether to generate mapping files that help developers navigate to original source files through navigation features provided by IDEs like “Go to Definition.”

As learned earlier, if you generate type declaration files using the declaration option, the following results are compiled:

// math.ts (original source file)
export const add = (x: number) => (y: number) => x + y;
// math.js (JavaScript)
export var add = function (x) { return function (y) { return x + y; }; };
// math.d.ts (type declaration)
export declare const add: (x: number) => (y: number) => number;

At this time, if there’s no mapping file defining the relationship between JavaScript code containing actually executed code and type declaration files containing type declarations, when using navigation features in IDEs, you navigate to type declarations in math.d.ts files, not source code.

But since information about type declarations is already exposed in developers’ own source code anyway, when using navigation, most cases want to see actual internal implementation of functions rather than wanting to know type declaration definitions.

In this situation, using the declarationMap option, when developers use navigation features, you can generate separate mapping files together so they can navigate to source code, not type declarations.

// math.d.ts (type declaration)

export declare const add: (x: number) => (y: number) => number;
//# sourceMappingURL=math.d.ts.map
// math.d.ts.map
{
  "version":3,
  "file":"math.d.ts",
  "sourceRoot":"",
  "sources":["../../../utils/math.ts"],
  "names":[],
  "mappings":"AAAA,eAAO,MAAM,GAAG,MAAO,MAAM,SAAS,MAAM,WAAU,CAAC"
}

Mapping files are composed in JSON format and created in the same path as files where type declarations are defined. Since these mapping files have original source code paths, when using IDE navigation features, they can inform “source code is not here but at this path.”

In other words, to fully utilize this feature, source code must necessarily be included when deploying modules to npm registry. Generally when deploying to npm registry, only build results are deployed rather than source code. In this case, since source code isn’t included in the library anyway, including mapping files becomes meaningless.

Simply including source code when deploying to npm registry doesn’t increase bundle size of applications using modules you made, so to greatly improve productivity and convenience of developers using libraries you made, I recommend turning on declarationMap and deploying to npm registry including source code.

downlevelIteration

Value Description
true Generate clear implementations for iteration features added in ES6
false (default) Perform only basic transpiling

The downlevelIteration option is an option that can make TypeScript transpile features added in ES6 like for/of, Spread, Symbol.iterator more clearly.

If the JavaScript version as compilation target is ES6 or higher, this option isn’t very meaningful, but when targeting ES5 or lower versions for cross-browsing, etc., you can prevent iterators from operating differently from developers’ intentions during JavaScript runtime.

For example, if transpiling TypeScript code using for/of to ES5, you can see results like below:

const str = 'Hello!';
for (const s of str) {
  console.log(s);
}
'use strict';
var str = 'Hello!';
for (var _i = 0, str_1 = str; _i < str_1.length; _i++) {
  var s = str_1[_i];
  console.log(s);
}

Since for/of is a feature not in ES5, TypeScript transpiled for/of into a general for statement.

Looking up to here seems no problems, but actually code transpiled like this doesn’t show behavior exactly matching original code. Because of code like this:

const str = '🙏';
for (const s of str) {
  console.log(s);
}

The 🙏 emoji used in the above code is an emoji I personally use a lot. Since the number of visibly seen characters is one, you might naturally think this emoji’s length is also 1, but actually emojis’ lengths aren’t 1.

'🙏'.length // 2
'👩‍❤️‍💋‍👩'.length // 11

In other words, writing general for statements using this emoji’s length can make it difficult to extract proper characters. If you don’t quite understand what this means, try copy-pasting and executing the code below in Chrome developer tools.

const str = "🙏";

for (let i = 0; i < str.length; i++) {
  console.log(`for문 > ${str[i]}`);
}

for (const s of str) {
  console.log(`for/of문 > ${s}`);
}
for문 > �
for문 > �

for/of문 > 🙏

This is exactly why you shouldn’t just transpile for/of statements into for statements.

To simply explain why emoji lengths aren’t 1 like this - first because emojis are multibyte characters, and also because new emojis keep being added and emojis combining existing emojis appear, methods of expressing emojis have become crazy. For details about emoji lengths, read this post which explains it well.

Anyway, the important point here is that such disasters can occur if you transpile them buried together just because for/of and for operations are similar.

So TypeScript separately provides an option called downlevelIteration so iteration features like for/of, Spread, Symbol.iterator don’t operate differently from developers’ intentions - it can check whether features like Symbol.iterator exist or even add polyfills implementing such features.

emitBOM

Value Description
true TypeScript marks BOM when generating output files
false (default) TypeScript doesn’t mark BOM when generating output files

The emitBOM option is an option to decide whether TypeScript marks BOM (Byte Order Mark) when generating output files.

Byte Order Mark is a method of indicating what encoding method this file used by adding special Unicode at the very front part of files.

Since BOM is used only to inform computers about current file’s encoding information, not for humans to read originally, text editors or places like vim don’t show BOM even when opening files.

However, generally in runtime environments where JavaScript executes, cases needing BOM are rare, and since Unicode 3.2 recommends not using BOM, it’s an option you don’t really need to turn on. (TypeScript official documentation also says you don’t need to turn it on)

emitDeclarationOnly

Value Description
true Output only type declaration files without JavaScript
false (default) Output including JavaScript files

The emitDeclarationOnly option, as its name Declaration only suggests, is an option deciding whether to output only type declaration files without JavaScript files when proceeding with compilation.

Usually used when using separate tools rather than TypeScript compiler when converting TypeScript to JavaScript, or when needing to provide only type declarations to modules already made in JavaScript.

importHelpers

Value Description
true Use helper functions provided by tslib in output files
false (default) Don’t use helper functions provided by tslib and implement helpers directly

The importHelpers option is an option to decide whether to directly write helper functions occurring when transpiling TypeScript to JavaScript versions ES5 or lower in output files, or allow replacement with helper functions provided by the tslib library.

Let’s understand by looking at source code and output files together.

export function fn(arr: number[]) {
  return [1, ...arr];
}

The fn function is a simple function that adds the element 1 at the very front of the array received as an argument and returns it. As you know, since the Spread feature usable with syntax like [...arr] isn’t included in ES5, TypeScript adds a helper function called __spreadArray in output files to transpile the Spread feature.

var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
  if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
    if (ar || !(i in from)) {
      if (!ar) ar = Array.prototype.slice.call(from, 0, i);
      ar[i] = from[i];
    }
  }
  return to.concat(ar || Array.prototype.slice.call(from));
};
export function fn(arr) {
  return __spreadArray([1], arr, true);
}

What’s important here is that implementation of the function __spreadArray was included together in output files to implement ES6’s Spread feature. Actually including such implementations in output files may not be a big problem, but if you need to use that __spreadArray again elsewhere, implementation of the __spreadArray function is added again to that module, causing duplicate code.

At this time, using the importHelpers option, you can change to get these helper functions from the tslib library to remove code duplication.

import { __spreadArray } from 'tslib';
export function fn(arr) {
  return __spreadArray([1], arr, true);
}

This way, since the __spreadArray function is imported from tslib and used, the need to declare the __spreadArray function every time disappears, so code duplication can be removed. But code transpiled using this option naturally needs the tslib package installed to work properly, so carefully weigh pros and cons of allowing duplicate implementation of helper functions versus installing tslib in your application and use the option.

importsNotUsedAsValues

Value Description
remove Remove import statements not needed at runtime from output files
preserve Remove type information from output files but keep import statements
error Generate errors when using import statements that only get type information

The importsNotUsedAsValues option is an option to control how TypeScript processes unnecessary import statements when generating output files. To understand the meaning this option has, you need to know a bit about how TypeScript processes import statements.

Basically, when TypeScript generates JavaScript files through compilation, it erases information related to types. Let’s understand by looking at code.

import { InterfaceFoo } from '../utils/foo';
import { ClassBar } from '../utils/bar';

const classBar: InterfaceFoo = new ClassBar();
import { ClassBar } from '../utils/bar';
var classBar = new ClassBar();

Looking at the above code, you can see the import statement getting the InterfaceFoo interface disappeared in JavaScript output files. Because JavaScript doesn’t have features like TypeScript’s types or interfaces, so leaving type information is meaningless.

Of course, as mentioned earlier, since type information can’t be used in JavaScript anyway, removing such information is correct, but if the ../utils/foo module includes intentional side effects, problems can occur.

// utils/foo.ts
export interface InterfaceFoo {}
console.log('hello world!');

The utils/foo.ts module is a module only exposing interfaces, but internally includes side effects of console.log. In this example it’s simple console output, but actually Angular includes side effects of explicitly injecting and registering modules inside such modules.

The problem is that even in such cases, TypeScript erases the statement itself that imported the utils/foo module when outputting JavaScript. Then naturally the side effect of console.log doesn’t execute in JavaScript runtime. So in such cases, developers must add one more import statement that can deceive TypeScript to intentionally execute side effects.

import { InterfaceFoo } from '../utils/foo';
import '../utils/foo';
import { ClassBar } from '../utils/bar';

const classBar: InterfaceFoo = new ClassBar();
import './utils/foo';
import { ClassBar } from "../utils/bar";
var classBar = new ClassBar();

So TypeScript version 3.8 added import type functionality to explicitly express “this import statement is a statement that only gets type information,” and provides the importsNotUsedAsValues option to control leaving import statements even if getting only type information using general import statements.

inlineSourceMap

Value Description
false Generate source map files separately
true Encode source map file contents in Base64 and add to source files

The inlineSourceMap option is an option deciding how TypeScript will generate source maps during compilation. Basically TypeScript provides source maps in the form of *.js.map files, but if inlineSourceMap is true, it adds source maps as comments inside source files.

Let’s directly compile modules having simple functions and see what differences there are.

// math.ts
export const add = (x: number) => (y: number) => x + y;

If the inlineSourceMap option is off, TypeScript generates separate source map files and generates source maps by writing only that source map’s path in compiled JS files.

// math.js
export var add = function (x) { return function (y) { return x + y; }; };
//# sourceMappingURL=math.js.map
// math.js.map
{"version":3,"file":"math.js","sourceRoot":"","sources":["../../utils/math.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,IAAM,GAAG,GAAG,UAAC,CAAS,IAAK,OAAA,UAAC,CAAS,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,EAApB,CAAoB,CAAC"}

At this time, you can confirm that source map file paths are added to compiled JS files in the form of #sourceMappingURL=math.js.map, and source map files have original TypeScript source file paths written like "sources": ["../../utils/math.ts"].

If you turn on the inlineSourceMap option, TypeScript no longer generates source map files and directly encodes source map file contents in Base64 and adds them inside compiled JS files.

export var add = function (x) { return function (y) { return x + y; }; };
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3V0aWxzL21hdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLElBQU0sR0FBRyxHQUFHLFVBQUMsQ0FBUyxJQUFLLE9BQUEsVUFBQyxDQUFTLElBQUssT0FBQSxDQUFDLEdBQUcsQ0FBQyxFQUFMLENBQUssRUFBcEIsQ0FBb0IsQ0FBQyJ9

inlineSources

Value Description
false Don’t include source code contents in inline source maps.
true Include source code contents together in inline source maps

The inlineSources option decides whether to also include original source code contents in inline source maps made using the inlineSourceMap option.

If this option’s value is true, a field called sourceContent is added to inline source maps and source code contents are included together in that field.

export var add = function (x) { return function (y) { return x + y; }; };
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3V0aWxzL21hdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLElBQU0sR0FBRyxHQUFHLFVBQUMsQ0FBUyxJQUFLLE9BQUEsVUFBQyxDQUFTLElBQUssT0FBQSxDQUFDLEdBQUcsQ0FBQyxFQUFMLENBQUssRUFBcEIsQ0FBb0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBjb25zdCBhZGQgPSAoeDogbnVtYmVyKSA9PiAoeTogbnVtYmVyKSA9PiB4ICsgeTtcbiJdfQ==
// Decoded source map

{
  "version":3,
  "file":"math.js",
  "sourceRoot":"",
  "sources":["../../utils/math.ts"],
  "names":[],
  "mappings":"AAAA,MAAM,CAAC,IAAM,GAAG,GAAG,UAAC,CAAS,IAAK,OAAA,UAAC,CAAS,IAAK,OAAA,CAAC,GAAG,CAAC,EAAL,CAAK,EAApB,CAAoB,CAAC",
  "sourcesContent":["export const add = (x: number) => (y: number) => x + y;\n"]
}

Originally in source map specs, the sourceContent field is close to a kind of exception handling to handle cases where original source files can’t be accessed, so it’s an option without big problems even if not turned on.

noEmit

Value Description
false Export output files after compilation
true Don’t export output files after compilation

The noEmit option, as its name suggests, decides behavior about whether TypeScript will export output files after compilation. Just hearing the explanation makes you wonder if such cases are needed, but it’s an option used surprisingly frequently and usefully.

Representative examples include registering commands like tsc --noEmit as NPM scripts when needing to only proceed with static type checking at timings like CI or Git Hook, or when using tools like Webpack, Parcel, Rollup for compilation, many cases use this option to only have TSC do static type checking.

noEmitHelpers

Value Description
false Include helper functions like __awaiter in compiled files
true Don’t include helper functions like __awaiter in compiled files

The noEmitHelpers option decides whether to include helper functions like __awaiter or __generator in output files where compilation completed.

Since the noEmitHelpers option’s operation is similar to importHelpers, it can be a bit confusing. If the importHelpers option decides whether to “include in source” or “import from elsewhere” implementations of helper functions, the noEmitHelpers option decides whether to “include in source” or “not do at all” implementations of helper functions.

Let’s understand this difference by looking at compiled code directly.

// When both importHelpers and noEmitHelpers are off, output files include helper implementations too

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  // ...
};
var __generator = (this && this.__generator) || function (thisArg, body) {
  // ...
};

export function foo() {
  return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
    return [2 /*return*/];
  }); });
}
// When importHelper is on, helpers are imported externally

import { __awaiter, __generator } from "tslib";
export function foo() {
  return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
    return [2 /*return*/];
  }); });
}
// When noEmitHelpers is on, there's no code even getting or declaring helpers at all

'use strict';
export function foo() {
  return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
    return [2 /*return*/];
  }); });
}

If both importHelpers and noEmitHelpers options are on, settings of whether to “get helper functions externally” or “not include in output files” conflict. In this case, importHelpers’ operation is followed preferentially, so be careful of this point.

noEmitOnError

Value Description
false Export output files even if errors occur during compilation
true Don’t export output files if errors occur during compilation

The noEmitOnError option, as its name suggests, decides whether to export output files if errors occur due to various factors during compilation. If this option is on, output files aren’t exported if errors occur during compilation.

Just looking like this, you might think “naturally shouldn’t output files be not made if errors occur?”, but development environments using TSC’s Watch option can be more convenient to generate output files even if errors occur during compilation and also observe side effects occurring from that, so I recommend turning this option off in development environments and turning it on at build time for production environment deployment.

preserveConstEnums

Value Description
false Remove Enum declarations using const enum keyword at compile time
true Don’t remove Enum declarations using const enum keyword at compile time

The preserveConstEnums option is an option about whether to remove Enum declarations using const enum keyword at compile time. To save memory costs at runtime, TypeScript replaces parts referencing values of Enums declared with const enum keyword with that Enum’s values.

Unlike Enums declared using only enum keyword, Enums using const enum keyword are guaranteed to only have constant values, so referential transparency is also guaranteed, and ultimately there’s no problem just replacing values at compile time.

const enum Foo {
  A = 1,
  B = 2,
  C = 3,
}
const foo = Foo.A;
var Foo;
(function (Foo) {
    Foo[Foo["A"] = 0] = "A";
    Foo[Foo["B"] = 1] = "B";
    Foo[Foo["C"] = 2] = "C";
})(Foo || (Foo = {}));
var foo = 1 /* A */; // <= Replaced with value, not Foo.A

At this time, looking closely at compiled code, a variable called Foo is declared to express Enum and values are assigned to that variable using IIFE, but you can see there are no parts accessing this variable. In other words, in runtime environments where this JS code executes, there’s no problem even without the variable Foo.

At this time, setting the preserveConstEnums option’s value to false can remove Enum declarations meaningless at runtime like this.

var foo = 1 /* A */;

Closing

I’ve wrapped up the fourth tsconfig series, the Emit edition. Options playing roles related to Emit are mainly touched when using TSC combined with other tools or when needing to optimize bundle size. Since I also studied these options after quite a while, even though there were options I’d used before, my memory seemed hazy.

That’s all for this post on [All About tsconfig] Compiler options / Emit.

관련 포스팅 보러가기

Aug 22, 2021

[Everything About tsconfig] Compiler Options / Modules

Programming/Tutorial/JavaScript
Aug 08, 2021

[Everything About tsconfig] Compiler Options / Type Checking

Programming/Tutorial/JavaScript
Jul 30, 2021

[Everything About tsconfig] Root Fields

Programming/Tutorial/JavaScript
Feb 07, 2026

Beyond Functors, All the Way to Monads

Programming
Jan 25, 2026

Why Do Type Systems Behave Like Proofs?

Programming