HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

8 Guide to tsconfig.json

Icon “details”Version: TypeScript 5.8

This chapter covers tsconfig.json as supported by TypeScript 5.8.

This chapter documents all common options of the TypeScript configuration file tsconfig.json:

8.1 Features not covered by this chapter

This chapter only describes how to set up projects whose local modules are all ESM. It does give tips for importing CommonJS, though.

Not explained here:

8.2 Extending base files via extends

This option lets us refer to an existing tsconfig.json via a module specifier (as if we imported a JSON file). That file becomes the base that our tsconfig extends. That means that our tsconfig has all the option of the base, but can override any of them and can add options not mentioned in the base.

The GitHub repository tsconfig/bases lists bases that are available under the npm namespace @tsconfig and can be used like this (after they were installed locally via npm):

{
  "extends": "@tsconfig/node-lts/tsconfig.json",
}

Alas, none of these files suit my needs. But they can serve as an inspiration for your tsconfig.

8.3 Where are the input files?

{
  "include": ["src/**/*"],
}

On one hand, we have to tell TypeScript what the input files are. These are the available options:

8.4 What is the output?

8.4.1 Where are the output files written?

"compilerOptions": {
  "rootDir": "src",
  "outDir": "dist",
}

How TypeScript determines where to write an output file:

As an example, consider the following tsconfig.json:

{
  "include": ["src/**/*"],
  "compilerOptions": {
    // Specify explicitly (don’t derive from source file paths):
    "rootDir": "src",
    "outDir": "dist",
    // ···
  }
}

Consequences of these settings:

8.4.1.1 Putting src/ and test/ next to each other

I like the idea of having a separate directory test/ that is a sibling of src/. However then the output files in dist/ are more deeply nested inside the project’s directory than the input files in src/ and test/. That means that we can’t access files such as package.json via relative module specifiers.

tsconfig.json:

{
  "include": ["src/**/*", "test/**/*"],
  "compilerOptions": {
    "rootDir": ".",
    "outDir": "dist",
    // ···
  }
}

Consequences of these settings:

8.4.1.2 Default values of rootDir

The default value of rootDir depends on the input file paths. I find that too unpredictable and always specify it explicitly. It is the longest common prefix of the input file paths.

Example 1: Default value is 'src' (relative to the project directory)

tsconfig.json:

{
  "include": ["src/**/*"],
}

Files:

/tmp/my-proj/
  tsconfig.json
  src/
    main.ts
    test/
      test.ts
  dist/
    main.js
    test/
      test.js

Example 2: Default value is 'src/core/cli'

tsconfig.json:

{
  "include": ["src/**/*"],
}

Files:

/tmp/my-proj/
  tsconfig.json
  src/
    core/
      cli/
        main.ts
        test/
          test.ts
  dist/
    main.js
    test/
      test.js

Example 3:

tsconfig.json: Default value is '.'

{
  "include": ["src/**/*", "test/**/*"],
}

Files:

/tmp/my-proj/
  tsconfig.json
  src/
    main.ts
  test/
    test.ts
  dist/
    src/
      main.js
    test/
      test.js

8.4.2 Emitting source maps

"compilerOptions": {
  "sourceMap": true,
}

sourceMap produces source map files that point from the transpiled JavaScript to the original TypeScript. That helps with debugging and is usually a good idea.

8.4.3 Emitting .d.ts files (e.g. for libraries)

If we want TypeScript code to consume our transpiled TypeScript code, we usually should include .d.ts files:

"compilerOptions": {
  "declaration": true,
  "declarationMap": true, // enables importers to jump to source
}

Optionally, we can include the TypeScript source code in our npm package and activate declarationMap. Then importers can, e.g., click on types or go to the definition of a value and their editor will send them to the original source code.

8.4.3.1 Option declarationDir

By default, each .d.ts file is put next to its .js file. If you want to change that, you can use option declarationDir.

8.4.4 Fine-tuning emitted files

"compilerOptions": {
  "newLine": "lf",
  "removeComments": false,
}

The values shown above are the defaults.

8.5 Language and platform features

"compilerOptions": {
  "target": "ESNext", // sets up "lib" accordingly
  "skipLibCheck": true,
}

8.5.1 target

target determines which newer JavaScript syntax is transpiled to older syntax. For example, if the target is "ES5" then an arrow function () => {} is transpiled to a function expression function () {}. Values can be:

"ESNext" means that nothing is ever transpiled. I find that setting easiest to deal with. It’s also the best setting if you don’t use tsc and use type stripping (which never transpiles anything either).

8.5.1.1 How to pick a good "ES20YY" target

If we want to transpile, we have to pick an ECMAScript version that works for our target platforms. There are two tables that provide good overviews:

Additionally, the official tsconfig bases all provide values for target.

8.5.2 lib

lib determines which types for built-in APIs are available – e.g. Math or methods of built-in types:

When does TypeScript support a given API? It must be “available un-prefixed/flagged in at least 2 browser engines (i.e. not just 2 chromium browsers)” (source).

8.5.2.1 Setting up lib via target

target determines the default value of lib: If the latter is omitted and target is "ES20YY" then "ES20YY.Full" is used. However, that is not a value we can use ourselves. If we want to replicate what removing lib does, we have to enumerate the contents of (e.g.) es2024.full.d.ts in the TypeScript source code repository ourselves:

/// <reference lib="es2024" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />

In this file, we can observe an interesting phenomenon:

Among other things, "DOM.Iterable" enables iteration over NodeLists – e.g.:

for (const x of document.querySelectorAll('div')) {}

8.5.3 skipLibCheck

8.5.4 Types for the built-in Node.js APIs

The types for the Node.js APIs must be installed via an npm package:

npm install @types/node

8.6 Module system

8.6.1 How does TypeScript look for imported modules?

These options affect how TypeScript looks for imported modules:

"compilerOptions": {
  "module": "NodeNext",
  "noUncheckedSideEffectImports": true,
}
8.6.1.1 Option module

With this option, we specify systems for handling modules. If we set it up correctly, we also take care of the related option moduleResolution, for which it provides good defaults. The TypeScript documentation recommends either of the following two values:

Given that bundlers mostly mimic what Node.js does, I’m always using "NodeNext" and haven’t encountered any issues.

Note that in both cases, TypeScript forces us to mention the complete names of local modules we import. We can’t omit filename extensions as was frequent practice when Node.js was only compiled to CommonJS. The new approach mirrors how pure-JavaScript ESM works.

module:NodeNext implies target:ESNext but in this case, I prefer to manually set up target because module and target are not as closely related as module and moduleResolution. Furthermore, module:Bundler does not imply anything.

8.6.1.2 Option noUncheckedSideEffectImports

By default, TypeScript does not complain if an empty import does not exist. The reason for this behavior is that this is a pattern supported by some bundlers to associate non-TypeScript artifacts with modules. And TypeScript only sees TypeScript files. This is what such an import looks like:

import './component-styles.css';

Interestingly, TypeScript normally is also OK with emptily imported TypeScript files that don’t exist. It only complains if we import something from a non-existent file.

import './does-not-exist.js'; // no error!

Setting noUncheckedSideEffectImports to true changes that. I’m explaining an alternative for importing non-TypeScript artifacts later.

8.6.2 Running TypeScript directly (without generating JS files)

"compilerOptions": {
  "allowImportingTsExtensions": true,
  // Only needed if compiling to JavaScript:
  "rewriteRelativeImportExtensions": true,
}

Most non-browser JavaScript platforms now can run TypeScript code directly, without transpiling it.

This mainly affects what filename extension we use when we import a local module. Traditionally, TypeScript does not change module specifiers and we have to use the filename extension .js in ESM modules (which is what works in the JavaScript that our TypeScript is compiled to):

import {someFunc} from './lib/utilities.js';

If we run TypeScript directly, that import statement looks like this:

import {someFunc} from './lib/utilities.ts';

This is enabled via the following settings:

Related option:

8.6.2.1 Node’s built-in support for TypeScript

Node.js now supports TypeScript via type stripping:

8.6.3 Importing JSON

"compilerOptions": {
  "resolveJsonModule": true,
}

The option resolveJsonModule enables us to import JSON files:

import data from './data.json' with {type: 'json'};
console.log(data.version);

8.6.4 Importing other non-TypeScript artifacts

Whenever we import a file basename.ext whose extension ext TypeScript doesn’t know, it looks for a file basename.d.ext.ts. If it can’t find it, it raises an error. The TypeScript documentation has a good example of what such a file can look like.

There are two ways in which we can prevent TypeScript from raising errors for unknown imports.

First, we can use option allowArbitraryExtensions to prevent any kind of error reporting in this case.

Second, we can create an ambient module declaration with a wildcard specifier – a .d.ts file that has to be somewhere among the files that TypeScript is aware of. The following example suppresses errors for all imports with the filename extension .css:

// ./src/globals.d.ts
declare module "*.css" {}

8.7 Type checking

"compilerOptions": {
  "strict": true,
  "exactOptionalPropertyTypes": true,
  "noFallthroughCasesInSwitch": true,
  "noImplicitOverride": true,
  "noImplicitReturns": true,
  "noPropertyAccessFromIndexSignature": true,
  "noUncheckedIndexedAccess": true,
}

strict is a must, in my opinion. With the remaining settings, you have to decide for yourself if you want the additional strictness for your code. You can start by adding all of them and see which ones cause too much trouble for your taste.

8.7.1 strict

The compiler setting strict provides an important minimal setting for type checking. In principle, this setting would default to true but backward compatibility makes that impossible.

Icon “tip”The compiler option strict in a nutshell

strict basically means: Type-check as much as possible, as correctly as possible.

strict activates the following settings (which won’t be mentioned again in this chapter):

8.7.2 exactOptionalPropertyTypes

If true then .colorTheme can only be omitted and not be set to undefined:

interface Settings {
  // Absent property means “system”
  colorTheme?: 'dark' | 'light';
}
const obj1: Settings = {}; // allowed
// @ts-expect-error: Type '{ colorTheme: undefined; }' is not
// assignable to type 'Settings' with
// 'exactOptionalPropertyTypes: true'. Consider adding 'undefined'
// to the types of the target's properties.
const obj2: Settings = { colorTheme: undefined };

This option also prevents optional tuple elements being undefined (vs. missing):

const tuple1: [number, string?] = [1];
const tuple2: [number, string?] = [1, 'hello'];
// @ts-expect-error: Type '[number, undefined]' is not assignable to
// type '[number, string?]'.
const tuple3: [number, string?] = [1, undefined];
8.7.2.1 exactOptionalPropertyTypes prevents useful patterns

I’m ambivalent about this option: On one hand, enabling it prevents useful patterns such as:

type Obj = {
  num?: number,
};
function createObj(num?: number): Obj {
  // @ts-expect-error: Type '{ num: number | undefined; }' is not
  // assignable to type 'Obj' with
  // 'exactOptionalPropertyTypes: true'.
  return { num };
} 
8.7.2.2 exactOptionalPropertyTypes produces better types: spreading

On the other hand, it does better reflect how JavaScript works – e.g., spreading distinguishes missing properties and properties whose values are undefined:

const optionDefaults: { a: number } = { a: 1 };
// This assignment is an error with `exactOptionalPropertyTypes`
const options: { a?: number } = { a: undefined }; // (A)

const result = { ...optionDefaults, ...options };
assertType<
  { a: number }
>(result);
assert.deepEqual(
  result, { a: undefined }
);

If we had assigned an empty object in line A then the value of result would be {a:1} and match its type.

Object.assign() works similarly to spreading.

8.7.2.3 exactOptionalPropertyTypes produces better types: in operator
function f(obj: {prop?: number}): void {
  if ('prop' in obj) {
    // Without `exactOptionalPropertyTypes`, the type would be:
    // number | undefined
    assertType<number>(obj.prop);
  }
}

8.7.3 noFallthroughCasesInSwitch

If true, non-empty switch cases must end with break, return or throw.

8.7.4 noImplicitOverride

If true then methods that override superclass methods must have the override modifier.

8.7.5 noImplicitReturns

If true then an “implicit return” (the function or method ending) is only allowed if the return type is void.

8.7.6 noPropertyAccessFromIndexSignature

If true then for types such as the following one, we cannot use the dot notation for unknown properties, only for known ones:

interface ObjectWithId {
  id: string,
  [key: string]: string;
}
function f(obj: ObjectWithId) {
  const value1 = obj.id; // allowed
  const value2 = obj['unknownProp']; // allowed
  // @ts-expect-error: Property 'unknownProp' comes from an index
  // signature, so it must be accessed with ['unknownProp'].
  const value3 = obj.unknownProp;
}

8.7.7 noUncheckedIndexedAccess

8.7.7.1 noUncheckedIndexedAccess and objects

If noUncheckedIndexedAccess is true then the type of an unknown property is the union of undefined and the type of the index signature:

interface ObjectWithId {
  id: string,
  [key: string]: string;
}
function f(obj: ObjectWithId): void {
  assertType<string>(obj.id);
  assertType<undefined | string>(obj['unknownProp']);
}

noUncheckedIndexedAccess does the same for Record (which is a mapped type):

function f(obj: Record<string, number>): void {
  // Without `noUncheckedIndexedAccess`, this type would be:
  // number
  assertType<undefined | number>(obj['hello']);
}
8.7.7.2 noUncheckedIndexedAccess and Arrays

Option noUncheckedIndexedAccess also affects how Arrays are handled:

const arr = ['a', 'b'];
const elem = arr[0];
// Without `noUncheckedIndexedAccess`, this type would be:
// string
assertType<undefined | string>(elem);

One common pattern for Arrays is to check the length before accessing an element. However, that pattern becomes inconvenient with noUncheckedIndexedAccess:

function logElemAt0(arr: Array<string>) {
  if (0 < arr.length) {
    const elem = arr[0];
    assertType<undefined | string>(elem);
    console.log(elem);
  }
}

Therefore, it makes more sense to use a different pattern:

function logElemAt0(arr: Array<string>) {
  if (0 in arr) {
    const elem = arr[0];
    assertType<string>(elem);
    console.log(elem);
  }
}

8.7.8 Type checking options that have good defaults

By default, the following options produce warnings in editors, but we can also choose to produce compiler errors or ignore problems:

8.8 Compiling TypeScript with tools other than tsc

The TypeScript compiler tsc performs three tasks:

  1. Type checking
  2. Emitting JavaScript files
  3. Emitting declaration files

External tools have become popular that do #2 and #3 much faster. The following subsections describe configuration options that help those tools.

8.8.1 Using tsc only for type checking

"compilerOptions": {
  "noEmit": true,
}

Sometimes, we want to use tsc only for type checking – e.g., if we run TypeScript directly or use external tools for compiling TypeScript files (to JavaScript files, declaration files, etc.):

In principle, you don’t have to provide output-related settings such as rootDir and outDir anymore. However, some external tools may need them.

8.8.2 Generating .js files via type stripping: erasableSyntaxOnly and verbatimModuleSyntax

"compilerOptions": {
  "erasableSyntaxOnly": true,
  "verbatimModuleSyntax": true, // implies "isolatedModules"
}

Type stripping is a simple and fast way of compiling TypeScript to JavaScript. It’s what Node.js uses when it runs TypeScript. Type stripping is fast because it only supports a subset of TypeScript where two things are possible:

  1. Type syntax can be detected and removed by only parsing the syntax – without performing additional semantic analyses.

  2. No non-type language features are transpiled. In other words: Removing the type syntax is enough to produce JavaScript.

To help with type stripping, TypeScript has two compiler options that report errors if we use unsupported features:

Note that these options don’t change what is emitted by tsc.

Useful related knowledge: “Type stripping technique: replacing types with spaces” (§6.5.1.1).

8.8.3 erasableSyntaxOnly: no transpiled language features

The compiler option erasableSyntaxOnly helps with type stripping. It forbids non-type TypeScript features that are not “current” JavaScript (as supported by the target platforms) and have to be transpiled. These are the most important ones:

Another feature that is forbidden by erasableSyntaxOnly is the legacy way of casting via angle brackets – because its syntax makes type stripping impossible in some cases (source):

<someType>someValue // not allowed

However, the alternative as is always better anyway:

someValue as someType // allowed

8.8.4 verbatimModuleSyntax: enforcing type in imports and exports

The compiler option verbatimModuleSyntax forces us to add the keyword type to type-only imports and exports.

When compiling TypeScript to JavaScript via type stripping, we need to remove the TypeScript parts. Most of those parts are easy to detect. The exception are imports and exports – e.g., without semantic analysis, we don’t know if an import is a (TypeScript) type or a (JavaScript) value. If type-only imports and exports are marked with the keyword type, no such analysis is necessary.

8.8.4.1 Importing types

This is what the keyword type looks like in imports:

// Input: TypeScript
import { type SomeInterface, SomeClass } from './my-module.js';

// Output: JavaScript
import { SomeClass } from './my-module.js';

Note that a class is both a value and a type. In that case, no type keyword is needed because that part of the syntax can stay in plain JavaScript.

We can also apply type to the whole import:

import type { Type1, Type2, Type3 } from './types.js';
8.8.4.2 Exporting types

Inline type exports:

export type MyType = {};
export interface MyInterface {}

Export clauses:

type Type1 = {};
type Type2 = {};
export {
  type Type1,
  type Type2,
}

type Type3 = {};
type Type4 = {};
export type {
  Type3,
  Type4,
}

Alas, default-exporting only works for interfaces:

export default interface DefaultInterface {} // OK

type DefaultType = {}
export default DefaultType; // error
export default type DefaultType; // error

export default type {} // error

We can use the following workaround:

type DefaultType = {}
export {
  type DefaultType as default,
}

Why does this inconsistency exist? type is allowed as a (JavaScript-level) identifier after export default.

8.8.4.3 isolatedModules

Activating verbatimModuleSyntax also activates isolatedModules, which is why we only need the former setting. The latter prevents us from using some relatively obscure features that are also problematic.

As an aside, this option enables esbuild to compile TypeScript to JavaScript (source).

8.8.5 isolatedDeclarations: generating .d.ts files more efficiently

"compilerOptions": {
  // Only allowed if `declaration` or `composite` are true
  "isolatedDeclarations": true,
}

Option isolatedDeclarations helps external tools compile TypeScript files to declaration files, by forcing us to add more type annotations so that no type inference is needed for compilation (trivially simple type inference is still allowed – more on that soon). That has several benefits:

isolatedDeclarations only produces compiler errors, it does not change what is emitted by tsc. It only affects constructs that are exported – because only those show up in declaration files. Module-internal code is not affected.

Let’s look at three constructs affected by isolatedDeclarations.

8.8.5.1 Return types of top-level functions

For top-level functions, we should usually explicitly specify return types:

// OK: return type stated explicitly
export function f1(): string {
  return 123..toString();
}
// Error: return type requires inference
export function f2() {
  return 123..toString();
}
// OK: return type trivial to determine
export function f3() {
  return 123;
}
8.8.5.2 Types of variable declarations

More complicated variable declarations must have type annotations. Note that this only affects top-level declarations – e.g.: Variable declarations inside functions don’t show up in declaration files and therefore don’t matter.

// OK: type trivial to determine
export const value1 = 123;
// Error: type requires inference
export const value2 = 123..toString();
// OK: type stated explicitly
export const value3: string = 123..toString();
8.8.5.3 Types of class instance fields

Class instance fields must have type annotations (even though tsc can infer their types if there is an assignment in the constructor):

export class C {
  str: string; // required
  constructor(str: string) {
    this.str = str;
  }
}
8.8.5.4 isolatedDeclarations requires declaration or composite

I’d love to always use isolatedDeclarations, but TypeScript only allows it if option declaration or option composite are active. Jake Bailey explains why that is:

At the implementation level, isolatedDeclarations diagnostics are extra declaration diagnostics produced by the declaration transformer, which we only run when declaration is enabled.

Theoretically it could be implemented such that isolatedDeclarations enables those checks (the diagnostics actually come from us running the transformer and then throwing away the resulting AST), but it is a change from the original design.

8.8.5.5 Further reading

The TypeScript 5.5 release notes have a comprehensive section on isolated declarations.

8.9 Importing CommonJS from ESM

One key issue affects importing a CommonJS module from an ESM module:

Let’s look at two options that help.

8.9.1 allowSyntheticDefaultImports: type-checking default imports of CommonJS modules

This option only affects type checking, not the JavaScript code emitted by TypeScript: If active, a default import of a CommonJS module refers to module.exports (not module.exports.default) – but only if there is no module.exports.default.

This reflects how Node.js handles default imports of CommonJS modules (source): “When importing CommonJS modules, the module.exports object is provided as the default export. Named exports may be available, provided by static analysis as a convenience for better ecosystem compatibility.”

Do we need this option? Yes, but it’s automatically activated if moduleResolution is "bundler" or if module is "NodeNext" (which activates esModuleInterop which activates allowSyntheticDefaultImports).

8.9.2 esModuleInterop: better compilation of TypeScript to CommonJS code

This option affects emitted CommonJS code:

Do we need this option? No, since we only author ESM modules.

8.10 One more option with a good default

We can usually ignore this option:

8.11 Visual Studio Code

If you are unhappy with the module specifiers for local imports in automatically created imports then you can take a look at the following two settings:

javascript.preferences.importModuleSpecifierEnding
typescript.preferences.importModuleSpecifierEnding

By default, VSC should now be smart enough to add filename extensions where necessary.

8.12 Summary: Assemble your tsconfig.json by answering four questions

This is a starter tsconfig.json file with all settings. The following subsections explain which parts to remove – depending on your needs.

Alternatively, you can use my interactive tsconfig configurator via the command line or online.

{
  "include": ["src/**/*"],
  "compilerOptions": {
    // Specified explicitly (not derived from source file paths)
    "rootDir": "src",
    "outDir": "dist",

    //========== Target and module ==========
    // Nothing is ever transpiled
    "target": "ESNext", // sets up "lib" accordingly
    "module": "NodeNext", // sets up "moduleResolution"
    // Don’t check .d.ts files
    "skipLibCheck": true,
    // Emptily imported modules must exist
    "noUncheckedSideEffectImports": true,
    // Allow importing JSON
    "resolveJsonModule": true,

    //========== Type checking ==========
    // Essential: activates several useful options
    "strict": true,
    // Beyond "strict": less important
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "noUncheckedIndexedAccess": true,

    //========== Only JS at non-type level (type stripping etc.) ==========
    // Forbid non-JavaScript language constructs such as:
    // JSX, enums, constructor parameter properties, namespaces
    "erasableSyntaxOnly": true,
    // Enforce keyword `type` for type imports etc.
    "verbatimModuleSyntax": true, // implies "isolatedModules"

    //========== Use filename extension .ts in imports ==========
    "allowImportingTsExtensions": true,
    // Only needed if compiling to JavaScript
    "rewriteRelativeImportExtensions": true, // from .ts to .js

    //========== Emitted files ==========
    // tsc only type-checks, doesn’t emit any files
    "noEmit": true,
    //----- Output: .js -----
    "sourceMap": true, // .js.map files
    //----- Output: .d.ts -----
    "declaration": true, // .d.ts files
    // “Go to definition” jumps to TS source etc.
    "declarationMap": true, // .d.ts.map files
    // - Enforces constraints that enable efficient .d.ts generation:
    //   no inferred return types for exported functions etc.
    // - Even though this option would be generally useful, it requires
    //   that `declaration` and/or `composite` are true.
    "isolatedDeclarations": true,
  }
}

8.12.1 Do you want to transpile new JavaScript to older JavaScript?

TypeScript can transpile new JavaScript features to code that only uses older “target” features. That can help support older JavaScript engines. If that’s what you want, you must change "target":

"compilerOptions": {
  // Transpile new JavaScript to old JavaScript
  "target": "ES20YY", // sets up "lib" accordingly
}

8.12.2 Should TypeScript only allow JavaScript features at the non-type level?

In other words: Should all non-JavaScript syntax be erasable? If yes, then these are the main features that are forbidden: JSX, enums, constructor parameter properties, and namespaces.

The starter tsconfig only allows erasable syntax. If you want to use any of the aforementioned features, then remove section “Only JS at non-type level”.

8.12.3 Which filename extension do you want to use in local imports?

The starter tsconfig enables .ts in imports. If you want to use .js, you can remove section “Use filename extension .ts in imports”.

8.12.4 What files should tsc emit?

If no files are emitted, you can remove the following properties:

Emitted files:

Filetsconfig.json
*.jsDefault (deactivated via "noEmit": true)
*.js.map"sourceMap": true
*.d.ts"declaration": true
*.d.ts.map"declarationMap": true

Source maps (.map) are only emitted if their source files are emitted.

8.13 Further reading

8.13.1 tsconfig.json recommendations by other people

8.13.2 Sources of this chapter

Some of the sources were already mentioned earlier. These are additional sources I used: