Setting Up: tsc, tsconfig.json and Running TypeScript
Installing TypeScript, compiling with tsc, configuring a project with tsconfig.json and strict mode, and the ways to run TypeScript code in Node.js.
Installing TypeScript
TypeScript is distributed as an npm package. You need Node.js and npm first. Install TypeScript as a development dependency of the project, together with the type definitions for Node.js:
npm install -D typescript @types/node
npx tsc --version
Version 7.0.2
-D (short for --save-dev) records both
packages under devDependencies in
package.json: they are needed to build the code, not to run it.
npx runs the
tsc that belongs to this project, from
node_modules/.bin. Since TypeScript 7 that command is a
native program: npm installs a small platform package (for example
@typescript/typescript-linux-arm64 on a 64-bit ARM
Linux machine) that contains the compiled binary.
@types/node describes Node.js's built-in modules and
globals such as process, so the checker knows them.
Pick the major version that matches the Node.js version you run in
production (@types/node@24 for Node.js 24).
Your editor needs no extra setup for the basics: Visual Studio Code and most other editors show TypeScript errors while you type, using the same checker.
Compiling with tsc
tsc is the TypeScript compiler's command-line program. Given TypeScript files it does two things: it type-checks them and reports errors, and it emits a JavaScript file for each one. For a single file in a folder without a configuration:
npx tsc meter.ts
node meter.js
Inside a project you almost never name files. Plain
npx tsc, without file names, reads
tsconfig.json (below) and compiles the whole project.
If you do name a file while a tsconfig.json is present,
TypeScript 7 stops with an error instead of silently ignoring your
configuration:
error TS5112: tsconfig.json is present but will not be loaded if files are specified on commandline. Use '--ignoreConfig' to skip this error.
Add --ignoreConfig when you really mean "just this
file, with default settings".
Error messages have the same parts everywhere: file, line and
column, a TS code and a description. In a terminal
tsc also prints the offending line with the problem
underlined; in a pipeline log, or with --pretty false,
each error is one line, which is the form the examples on this site
show:
meter.ts:4:28 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
4 console.log(addReading(10, "12.5"));
~~~~~~
Found 1 error in meter.ts:4
tsc ends with exit code 0 when there are no errors.
With errors it ends with 2 if it still wrote the JavaScript (the
default; see
What Is TypeScript?), and with 1 if it wrote nothing. Build scripts and pipelines rely
on that non-zero code to stop.
Checking without output
Often you only want the check. Bundlers, test tools and Node.js's
type stripping (below) already turn TypeScript into JavaScript, so
the compiler's own output is not needed.
noEmit is the option that tells
tsc to type-check without writing any files; as a flag
it is tsc --noEmit. It is the usual command in a
pre-commit hook and in a
CI pipeline.
Watch mode and project builds
tsc --watch (or -w) keeps running and
rechecks the project every time you save a file. Large code bases
split into several projects that depend on each other use
tsc -b (build mode), which builds them in the right
order and skips the ones that have not changed.
tsconfig.json
tsconfig.json is the
configuration file of a TypeScript project. Its presence marks the
folder as the project root; it says which files belong to the
project, which checks to apply and what JavaScript to produce.
npx tsc --init writes a starting file. In TypeScript
7.0 that file is short and has sensible current settings, with
comments that explain the options:
{
// 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
…
"strict": true,
…
}
}
As you can see, tsconfig.json may contain comments and
trailing commas, which plain
JSON does not
allow. For a small Node.js service, a complete configuration can
look like this:
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"module": "nodenext",
"target": "es2023",
"lib": ["es2023"],
"types": ["node"],
"strict": true,
"noUncheckedIndexedAccess": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src"]
}
With this file, npx tsc compiles every
.ts file under src/ into
dist/, keeping the folder structure. Together with
"type": "module" in package.json (see
below), the output consists of
ES modules.
The options that matter
| Option | What it does |
|---|---|
include, rootDir,
outDir
|
Which files are part of the project, where the sources live, and where the emitted JavaScript goes. |
module |
How modules work. "nodenext" follows Node.js's
own rules: a file becomes an ES module or a CommonJS module
depending on its extension and the nearest
package.json. Code that a bundler processes
uses "preserve" or "esnext".
|
target |
Which JavaScript edition the output may use. Newer syntax is
rewritten into older syntax for older targets. The default
in TypeScript 7 is es2025.
|
lib |
Which built-in APIs the checker assumes exist: an ECMAScript
edition, plus "dom" for browser code. It
follows target unless you set it.
|
types |
Which installed @types packages are loaded
automatically. Since TypeScript 6.0 the default is
[] (none), so a Node.js project must list
"node".
|
strict |
The family of strict checks, explained in the next section. On by default since 6.0; writing it out makes the intent clear. |
verbatimModuleSyntax,
isolatedModules
|
Reject code that only tsc can compile correctly
because it needs to look at other files. That keeps the code
usable by faster tools that handle one file at a time, such
as bundlers and type stripping.
|
sourceMap |
Writes a source map next to each output file, so stack traces and debuggers point at the TypeScript lines. |
skipLibCheck |
Skips checking the type definitions of installed packages. Faster, and it avoids errors in other people's code that you cannot fix. |
The full list, and the stricter options worth adding, are on Tooling, Configuration and Migrating from JavaScript.
target and lib in practice
lib protects you from using an API your runtime does
not have. The examples on this site are checked with
lib set to ES2023, so they also work on older runtimes
such as Node.js 20, which lacks Object.groupBy (added
in ES2024). The checker says so before the code can crash there,
even though the Node.js 24 that runs the examples does have it:
const tickets = [
{ id: "INC-30412", team: "Portal" },
{ id: "INC-30415", team: "NOC" },
];
const byTeam = Object.groupBy(tickets, (t) => t.team);
console.log(byTeam);
example.ts(5,23): error TS2550: Property 'groupBy' does not exist on type 'ObjectConstructor'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2024' or later.
Choose target and lib to match the oldest
runtime that will run your code: ES2023 is safe for Node.js 20 and
later; raise it when every server runs a newer Node.js.
types: ["node"] in turn makes Node.js's own modules
known. Here a plain .ts file uses
node:path and the global process:
import { basename, extname, join } from "node:path";
const file: string = join("reports", "2026-10", "E-1001.json");
console.log(file);
console.log(basename(file, extname(file)));
console.log(typeof process.exitCode);
reports/2026-10/E-1001.json E-1001 undefined
Without "types": ["node"] the same file fails with
error TS2591: Cannot find name 'process'. Do you need to install
type definitions for node?, followed by advice to install @types/node and add it
to types. This file has no
"type": "module" nearby, so with
module set to "nodenext" it is compiled as
CommonJS, and the import becomes a
require call in the output:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_path_1 = require("node:path");
const file = (0, node_path_1.join)("reports", "2026-10", "E-1001.json");
console.log(file);
In an ES module the import line stays as written. Both
module systems are explained in
Modules and npm.
Strict mode
Strict mode in
TypeScript is the compiler option strict, which
switches on a family of stricter type checks at once. It is a
different thing from JavaScript's
strict mode
("use strict"), which changes how code behaves at run
time; tsc emits that directive too, but it is not what
the option is about. In TypeScript 7.0, strict turns
on:
-
noImplicitAny: a parameter whose type cannot be inferred must be annotated (see implicit any). -
strictNullChecks:nullandundefinedare their own types and must be handled (see null, undefined and Strict Null Checks). -
useUnknownInCatchVariables: the error in acatchblock has the typeunknown. -
strictFunctionTypes,strictBindCallApply,noImplicitThis,strictPropertyInitializationandstrictBuiltinIteratorReturn: stricter rules for function assignments,bind/call/apply,this, class fields and built-in iterators.
Since TypeScript 6.0 strict defaults to
true; all examples on this site are checked with it.
Three of its checks in action, each followed by the fix. First,
parameters without types:
function averageKWh(readings) {
return readings.reduce((sum, r) => sum + r, 0) / readings.length;
}
console.log(averageKWh([11.2, 9.8, 12.5]));
example.ts(1,21): error TS7006: Parameter 'readings' implicitly has an 'any' type.
function averageKWh(readings: number[]): number {
return readings.reduce((sum, r) => sum + r, 0) / readings.length;
}
console.log(averageKWh([11.2, 9.8, 12.5]).toFixed(2));
11.17
Only readings needed a type: once it is known to be a
number[], the checker infers sum and
r in the callback by itself. Second, a value that may
be missing. Map.get returns undefined for
an unknown key, and strict mode makes you deal with that:
const tariffs = new Map([["day", 0.32], ["night", 0.25]]);
const price = tariffs.get("weekend");
console.log((4.2 * price).toFixed(2));
example.ts(3,20): error TS18048: 'price' is possibly 'undefined'.
const tariffs = new Map([["day", 0.32], ["night", 0.25]]);
const price = tariffs.get("weekend") ?? 0.32;
console.log((4.2 * price).toFixed(2));
1.34
The
?? operator
supplies a default, so price is always a number. Third,
caught errors. JavaScript can throw any value, not only
Error objects, so the checker does not let you assume
what you caught:
try {
JSON.parse("{ meterId: E-1001 }");
} catch (err) {
console.log(err.message);
}
example.ts(4,15): error TS18046: 'err' is of type 'unknown'.
try {
JSON.parse("{ meterId: E-1001 }");
} catch (err) {
if (err instanceof SyntaxError) {
console.log(`Bad JSON: ${err.name}`);
}
}
Bad JSON: SyntaxError
Options removed in TypeScript 6 and 7
TypeScript 6.0 deprecated a set of old options and changed several defaults; TypeScript 7.0 turned the deprecations into errors. Many tutorials, Stack Overflow answers and older projects still use them, so you will meet these messages when you upgrade or copy a configuration:
tsconfig.json(6,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(7,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
Use '"paths": {"*": ["./*"]}' instead.
tsconfig.json(8,15): error TS5108: Option 'target=ES5' has been removed. Please remove it from your configuration.
| Old setting | As of TypeScript 7 |
|---|---|
"target": "es5" or "es3",
downlevelIteration
|
Removed. The lowest target is ES2015; the default is ES2025. All current browsers and Node.js versions support ES2015. |
"moduleResolution": "node" (also called
node10) or "classic"
|
Removed. Use "nodenext" for code that Node.js
runs and "bundler" for code that a bundler
processes.
|
baseUrl |
Removed. Path aliases in paths work without it,
relative to the tsconfig.json.
|
outFile; "module" set to
amd, umd, system or
none
|
Removed. Combining files into one is a bundler's job now. |
"esModuleInterop": false,
"allowSyntheticDefaultImports": false,
"alwaysStrict": false
|
Can no longer be switched off; the behavior is always on. |
All @types packages load automatically |
types defaults to [] since 6.0:
list what you need, such as ["node"].
|
"strict": true must be added |
On by default since 6.0. Still worth writing out. |
Running TypeScript
Something has to remove the types before JavaScript can run. There are two families of tools: the compiler, which checks and then emits, and tools that only strip the types and run what is left.
tsc --noEmit separately, in the editor, before a
commit and in the pipeline.
Compile, then run
The classic route, and still the usual one for production builds:
npx tsc writes JavaScript to dist/, and
the server runs node dist/main.js. The production
machine needs only Node.js and the emitted files, not TypeScript.
This is how the examples on this site run: compiled with
tsc, then run with Node.js 24.
Node.js type stripping
Type stripping is running
TypeScript by removing its type syntax and executing the JavaScript
that is left, without checking the types. Node.js does this itself:
node meter.ts replaces every annotation with spaces (so
line and column numbers stay the same) and runs the file. The
feature arrived step by step:
| Node.js version | Type stripping |
|---|---|
| 20 and older |
Not available: node meter.ts fails with a
SyntaxError at the first piece of type syntax.
|
| 22.6 |
Added behind the flag
--experimental-strip-types.
|
| 22.18 and 23.6 | On by default. |
| 24.3 (and 22.18) | No longer prints an "experimental" warning. |
| 24.12 and 25.2 | Stable. |
As of October 2026, Node.js 20 has reached end of life (April 2026),
22 is in maintenance, 24 is the active long-term support (LTS) line,
and 26 becomes the next LTS at the end of October. On any supported
line, node meter.ts works. Its limits follow from
"strip, don't compile":
- No type checking. A file full of type errors runs anyway.
-
Only erasable syntax. TypeScript features that
generate JavaScript code, such as
enumand constructor parameter properties, are rejected (see Enums, const Assertions and satisfies). The compiler optionerasableSyntaxOnlymakestscreport them in advance. -
tsconfig.jsonis ignored, and imports must name the real file, including its extension:import { DAY } from "./tariffs.ts". Type-only imports must be written asimport type.
The Node.js documentation recommends a configuration like this for
projects that run with type stripping, so that tsc
acts purely as the checker (the last line,
"types": ["node"], is our addition, needed since
TypeScript 6.0):
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"rewriteRelativeImportExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"types": ["node"]
}
}
More about type stripping, and about the browser, where nothing runs TypeScript and a build tool is always involved, is in TypeScript in Node.js and the Browser.
tsx, Deno and Bun
-
tsx is an npm package that runs TypeScript files
on Node.js versions without type stripping, and also handles the
syntax Node.js refuses, such as enums. It uses the fast esbuild
transformer and does not check types:
npx tsx meter.ts, ornpx tsx watch meter.tsto restart on every change. -
Deno and Bun, the two
alternative JavaScript runtimes, run
.tsfiles directly (deno run meter.ts,bun meter.ts), also without checking types by default. Deno has its own checker command,deno check; Bun's documentation says to keep usingtscfor that.
Putting it together
A small Node.js project with the
tsconfig.json above needs only a few
lines in package.json. The scripts give every developer
and the pipeline the same commands (see
npm scripts):
{
"name": "meter-report",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"start": "node dist/main.js"
},
"devDependencies": {
"@types/node": "^24.0.0",
"typescript": "^7.0.2"
}
}
meter-report/
├── package.json
├── tsconfig.json
├── src/
│ ├── main.ts
│ └── tariffs.ts
└── dist/ ← written by npm run build, not committed
├── main.js
├── main.js.map
├── tariffs.js
└── tariffs.js.map
In this setup src/main.ts imports from
"./tariffs.js", the name of the file that will exist
after compiling; with module set to
"nodenext" TypeScript requires that extension. The
details are in
Modules, Declaration Files and Typing JavaScript Libraries.
npm run typecheck # check only: editor, pre-commit hook, pipeline
npm run build # check and write dist/
npm start # run the compiled code
Put dist/ in .gitignore: it is generated
from the sources and rebuilt by the pipeline.