[All About tsconfig] Compiler options / Emit
![[All About tsconfig] Compiler options / Emit [All About tsconfig] Compiler options / Emit](/static/d96f04d4b1d49fa98ee91007f9ad0674/01fb2/thumbnail.png)
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.jsonWhen declarationDir option is set to "./dist/types"
myProject
├── math.ts
├── dist
│ ├── math.js
│ └── types
│ └── math.d.ts <
└── tsconfig.jsonGathering 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 // 11In 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWF0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3V0aWxzL21hdGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsTUFBTSxDQUFDLElBQU0sR0FBRyxHQUFHLFVBQUMsQ0FBUyxJQUFLLE9BQUEsVUFBQyxDQUFTLElBQUssT0FBQSxDQUFDLEdBQUcsQ0FBQyxFQUFMLENBQUssRUFBcEIsQ0FBb0IsQ0FBQyJ9inlineSources
| 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.AAt 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.
관련 포스팅 보러가기
[Everything About tsconfig] Compiler Options / Modules
Programming/Tutorial/JavaScript[Everything About tsconfig] Compiler Options / Type Checking
Programming/Tutorial/JavaScript[Everything About tsconfig] Root Fields
Programming/Tutorial/JavaScriptBeyond Functors, All the Way to Monads
ProgrammingWhy Do Type Systems Behave Like Proofs?
Programming