tsconfig.json Explained: TypeScript 7 Compiler Options

What each tsconfig.json option does, the real tsc –init output of TypeScript 7.0.2, three stricter options worth enabling, and the old options TypeScript 7 removed.

angular-typescript

The tsconfig.json file is the configuration file of a TypeScript project, and it tells the TypeScript compiler (tsc) which files belong to the project and which compiler options to use for them. The file sits in the project’s root folder. When we run tsc without arguments, the compiler looks for tsconfig.json in the current folder and then in each parent folder, and compiles the project it finds.

We use tsconfig.json to pick the JavaScript version that tsc writes, the module format for Node.js or a bundler, the output folder and how strict the type checks are. For example, a Node.js REST service and a React front end in the same repository each get their own tsconfig.json with different module and lib settings.

The following example is a commented tsconfig.json for a Node.js project on TypeScript 7.0.2. A tsconfig.json file may contain comments, so each group of options carries a short note on what it controls.

{
  "compilerOptions": {
    // 1. Output JavaScript version and built-in API types
    "target": "es2024",
    "lib": ["es2024"],

    // 2. Module format and import resolution for Node.js
    "module": "nodenext",

    // 3. Source root and output folder
    "rootDir": ".",
    "outDir": "dist",

    // 4. Type checks
    "strict": true,
    "noUncheckedIndexedAccess": true,

    // 5. Global type packages (@types/node)
    "types": ["node"],

    // 6. Import rules and Node.js type stripping rules
    "verbatimModuleSyntax": true,
    "erasableSyntaxOnly": true,

    // 7. Do not check .d.ts files in node_modules
    "skipLibCheck": true
  },
  "include": ["src"]
}

Notice that strict is only one line, whereas noUncheckedIndexedAccess, verbatimModuleSyntax and erasableSyntaxOnly are separate options that strict does not turn on.

Next, we create a starter file with tsc –init and go through each option used in the howtodoinjava TypeScript example projects. After that, we add the stricter options, choose files with include and extends, and list the old options that TypeScript 7 removed, with the real error messages.

1. Creating tsconfig.json With tsc –init

The tsc –init command writes a starter tsconfig.json into the current folder. We run it once, inside the project, after installing TypeScript as a development dependency.

npm install --save-dev typescript@7.0.2
npx tsc --init

Older versions of tsc wrote a long file with most options commented out. The file from TypeScript 7.0.2 is short and contains the settings the TypeScript team recommends for new projects.

{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    // "rootDir": "./src",
    // "outDir": "./dist",

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "nodenext",
    "target": "esnext",
    "types": [],
    // For nodejs:
    // "lib": ["esnext"],
    // "types": ["node"],
    // and npm install -D @types/node

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,

    // Recommended Options
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,
  }
}

The generated file is a starting point, not a final config. For a Node.js application, we uncomment rootDir, outDir, lib and “types”: [“node”] and install @types/node. We can drop declaration and declarationMap, because they produce .d.ts files that only libraries need, and jsx, which matters only for React code. The exactOptionalPropertyTypes option is strict about the difference between a missing property and a property set to undefined. In an existing code base it often reports many errors, so we may turn it off there and fix the code later.

2. Options Used in the Example Projects

Every example project for the howtodoinjava TypeScript articles uses the same short config, shown in full in section 6. Each option in it answers one question about the project, such as which JavaScript version to write or where the output goes, and we go through them in the order they appear in the file.

2.1. target and lib

The target option sets the JavaScript version of the output. When our code uses syntax that the target does not have, tsc rewrites it into older syntax. With es2024, Node.js 22 runs every feature natively, so the output looks almost like the input. TypeScript 7 accepts es2015 up to es2025 and esnext, whereas es5 and es3 are gone.

The lib option controls something different, namely which built-in APIs the compiler knows about. It does not add any code to the output. With “lib”: [“es2023”], a call to the ES2024 method Map.groupBy() fails even though Node.js 22 has it.

src/a.ts(5,20): error TS2550: Property 'groupBy' does not exist on type 'MapConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2024' or later.

When lib is not set, it follows target, and it also includes the browser types (dom). Setting lib explicitly to [“es2024”] removes document and window from a Node.js project, so code that uses browser APIs by mistake does not compile. For example, a payment service that calls document.querySelector() by mistake fails at compile time instead of at runtime on the server. For browser code, we add “dom” to the list.

2.2. module Set to nodenext

The module option sets how import and export statements appear in the output and how the compiler finds imported files. The value nodenext follows the rules of current Node.js, so a file is an ES module when the nearest package.json contains “type”: “module”, or a CommonJS module otherwise. The value nodenext also implies moduleResolution nodenext, so we do not set that option separately.

Under these rules, relative imports need the file extension of the output file, which is .js even in a .ts file.

import { describe, type Fruit } from "./types.js";

When we leave out the extension, tsc reports error TS2835.

src/a.ts(1,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './types.js'?

For code that a bundler such as Vite processes, “module”: “preserve” is the matching value, and it allows imports without extensions. The older values amd, umd, systemjs and none no longer exist in TypeScript 7.

2.3. strict

The strict option turns on a group of checks at once, namely noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, strictBuiltinIteratorReturn, noImplicitThis, useUnknownInCatchVariables and alwaysStrict. Since TypeScript 6.0, strict is true by default, but we still write it in the config so that the intent is visible to the next developer who opens the file.

Two checks in the group cause most of the errors in new code. The noImplicitAny check requires a type on every parameter that TypeScript cannot infer, and the strictNullChecks check keeps undefined and null out of other types.

// 1. noImplicitAny: parameters need a type
const double = (n: number) => n * 2;
const result = double(4);                     // result = 8

// 2. strictNullChecks: find() can return undefined
const ages = [37, 35, 40];
const found = ages.find((age) => age > 38);   // found = 40, type number | undefined
const next = (found ?? 0) + 1;                // next = 41

Without the : number annotation, tsc reports “error TS7006: Parameter ‘n’ implicitly has an ‘any’ type.” Without the ?? 0, tsc reports that found is possibly undefined. Each strict check can be turned off on its own (for example “noImplicitAny”: false) while migrating old JavaScript code.

2.4. rootDir and outDir

The outDir option sets the folder for the generated files, and rootDir sets which part of the source path is kept in that folder. The compiler copies the folder structure below rootDir into outDir.

rootDirSource fileOutput file
“.”src/index.tsdist/src/index.js
“src”src/index.tsdist/index.js

Our example projects use “.”, which is why they run node dist/src/index.js. In TypeScript 7, rootDir defaults to the folder that contains tsconfig.json. A project whose sources are all in src must set rootDir, or the compiler stops with error TS5011.

tsconfig.json(12,5): error TS5011: The common source directory of 'tsconfig.json' is './src'. The 'rootDir' setting must be explicitly set to this or another path to adjust your output's file layout.
  Visit https://aka.ms/ts6 for migration information.

Note that rootDir does not choose which files are compiled. The include array does that job, as we will see in section 4.

2.5. types

The types option lists the @types packages whose global declarations are loaded, such as process and Buffer from @types/node. Since TypeScript 6.0 the default is an empty list, so a Node.js project must name the package. Without it, tsc does not know process.

src/a.ts(3,14): error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

The fix is npm install –save-dev @types/node and “types”: [“node”]. A test project would add its framework too, for example “types”: [“node”, “jest”]. The types option affects only globals, so types of packages we import with import load as before.

2.6. skipLibCheck

The skipLibCheck option stops the compiler from type-checking .d.ts files, mainly the ones in node_modules. Our own code is still checked against those declarations, but tsc does not report conflicts between two libraries’ declaration files. The skipLibCheck option makes builds faster and avoids errors we cannot fix in third-party code, so tsc –init turns it on, and almost every project uses it.

3. Three Stricter Options Worth Turning On

The options in this section are not part of strict. Each one prevents a specific kind of bug or build problem, and tsc –init turns on the first two by default. The example project compiles with all three through its second config file, tsconfig.strict.json.

3.1. noUncheckedIndexedAccess

By default, TypeScript assumes that an array index or a key of a Record always finds a value, so fruits[5] has the type string even when the array has two elements. The noUncheckedIndexedAccess option adds undefined to the type of every index access, so the compiler asks us to handle the missing case.

const fruits = ["apple", "banana"];
const prices: Record<string, number> = { apple: 5, banana: 3 };

// 1. Array element: type string | undefined
const first = fruits[0];
const upper = first?.toUpperCase();           // upper = "APPLE"

// 2. Missing key: type number | undefined
const cherry = prices["cherry"] ?? 0;         // cherry = 0

With the option on, assigning fruits[0] to a variable of type string fails with “error TS2322: Type ‘string | undefined’ is not assignable to type ‘string’.” The option adds some ?. and ?? operators to the code, but it catches off-by-one errors and missing keys at compile time. For example, a checkout page that reads prices[item] for an item missing from the price list would add undefined to the total and show NaN, and the option reports that line at compile time. A for…of loop does not need these checks, because it only visits existing elements.

3.2. verbatimModuleSyntax

The verbatimModuleSyntax option makes the compiler output every import and export as written, after removing only the ones marked with type. As a result, an import of something that is only a type must say so, or tsc reports error TS1484.

src/errors.ts(1,20): error TS1484: 'Fruit' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.

We fix the import with an inline type modifier or a separate import type line.

import { describe, type Fruit } from "./types.js";
import type { Fruit as Item } from "./types.js";

The type-only import rule matters for every tool that compiles one file at a time, such as esbuild or SWC, and for Node.js type stripping. Without the type marker, a one-file-at-a-time tool cannot know that Fruit does not exist at runtime, so it keeps the import, which fails when the code runs. With verbatimModuleSyntax turned on, tsc –showConfig also lists isolatedModules as enabled. In a CommonJS package (no “type”: “module”), the option reports import statements with error TS1295, because the output would keep them as written.

3.3. erasableSyntaxOnly

The erasableSyntaxOnly option, added in TypeScript 5.8, reports TypeScript syntax that produces JavaScript code instead of disappearing at compile time. The option reports these constructs.

  • An enum declaration.
  • A namespace with runtime code.
  • Constructor parameter properties such as constructor(private name: string).
  • The import x = require() syntax.
  • The export = syntax.

Node.js type stripping, which runs .ts files without a build step since Node.js 22.18, rejects the same constructs. For a team that runs its scripts with node script.ts, the option moves the error from the Node.js startup to the type check, where the editor shows it.

src/errors.ts(6,6): error TS1294: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.

An object with as const replaces most enums and needs no code generation.

const Size = { Small: "small", Large: "large" } as const;
type Size = (typeof Size)[keyof typeof Size];

const cup: Size = Size.Large;                 // cup = "large"

The second line creates a type with the same name as the object, so Size works both as a value (Size.Large) and as a type (“small” | “large”).

4. Choosing Files With include, exclude, files and extends

The top-level include array lists glob patterns for the files to compile. Our projects use [“src”], which means every .ts file below src. The exclude array removes files from that set, for example *[“src/**/.test.ts”], and files lists individual files when there are only a few. Without include and files, the compiler takes every .ts file under the folder of tsconfig.json, except node_modules and outDir.

The extends option loads another config and lets us override parts of it. The community project tsconfig/bases publishes ready-made configs per runtime, such as @tsconfig/node22. A company with ten Node.js services can extend one shared base and override only the folders in each service.

{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

Two command-line flags help when a config grows.

  • The command npx tsc –showConfig prints the final options after extends and the defaults are applied.
  • The command npx tsc -p tsconfig.strict.json compiles with a different config file.

Since TypeScript 7, passing file names to tsc in a folder that has a tsconfig.json is an error (TS5112) unless we also pass –ignoreConfig.

5. Options Removed in TypeScript 7

TypeScript 7.0 turned the options that TypeScript 6.0 deprecated into hard errors. Configs copied from older tutorials, including the earlier version of this page, often contain them. Each removed setting has a replacement in TypeScript 7.0.

Old settingWhat to use instead
“target”: “es5” (and es3)es2015 or newer, such as es2024
“moduleResolution”: “node” (node10) or “classic”Leave it out, because module nodenext implies it, or use bundler
“module”: “amd”, “umd”, “systemjs”, “none”nodenext for Node.js, esnext or preserve with a bundler
“baseUrl”: “…”paths entries relative to the project root
“downlevelIteration”: trueRemove it, because ES2015+ targets do not need it
“esModuleInterop”: false, “allowSyntheticDefaultImports”: falseRemove them, because both are always on
“alwaysStrict”: falseRemove it, because strict mode output is always on

The errors name the option, and some also name the replacement. We wrote the test configs on one line, so every error points to line 1.

tsconfig.json(1,50): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
tsconfig.json(1,119): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(1,100): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
  Use '"paths": {"*": ["./*"]}' instead.
tsconfig.json(1,120): error TS5108: Option 'esModuleInterop=false' has been removed. Please remove it from your configuration.

Options such as experimentalDecorators and emitDecoratorMetadata, which old Angular configs used, still exist. Standard decorators work without them since TypeScript 5.0, so new projects leave them out.

6. Building the Example With Both Configs

The tsconfig-json project ships two config files. The standard tsconfig.json is shared by all howtodoinjava TypeScript examples, and tsconfig.strict.json is the commented config from the top of this article. Both compile the same src folder with TypeScript 7.0.2 on Node.js 22.

{
  "compilerOptions": {
    "target": "es2024",
    "lib": ["es2024"],
    "module": "nodenext",
    "strict": true,
    "rootDir": ".",
    "outDir": "dist",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src"]
}
npm install
npm start               # compile with tsconfig.json and run
npm run check:strict    # type-check with tsconfig.strict.json
=== strict ===
result = 8 | found = 40 | next = 41
=== noUncheckedIndexedAccess ===
upper = APPLE | cherry = 0
=== verbatimModuleSyntax ===
text = apple costs 5
=== erasableSyntaxOnly ===
cup = large

The check:strict script prints nothing when the code passes all the stricter checks.

7. Conclusion

A tsconfig.json file decides which files tsc compiles and how. For a Node.js project on TypeScript 7, a short config covers most needs, with target and lib set to a recent ECMAScript version, module nodenext, strict checks, rootDir and outDir for the output layout, “types”: [“node”], and skipLibCheck. The stricter options from section 3 catch more bugs and keep the code compatible with Node.js type stripping.

Configs with removed settings such as target es5 or baseUrl need updating before they work with TypeScript 7. If the compiler itself is new to us, the first steps are installing and running tsc on a single file.

8. References

The TSConfig reference documents each option, and the release notes explain the TypeScript 6 and 7 changes to the defaults.

Happy Learning !!

Source Code on Github

Leave a Comment

  1. uh… cannot it be just made “global” instead of “per project” ?
    Why ? Because we are a team and everyone has his own personal preferences while we all do git check out the same project… then we all check in whatever we change.
    But we don’t want to have to recreate a tsconfig.json every time one wipes his local repo (there are some clumsy people) and this file should not go in the repo anyway….
    So, can it be created and tossed into my c:\Users\dude\tsconfig.json ?
    I’m gonna give it a try on this idea, but thanks for the post.

  2. Typo on the typescript install:

    $ npm uninstall typescript -g   //Install typescript
    

    should be

    $ npm install typescript -g   //Install typescript
    

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.