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: null and undefined are their own types and must be handled (see null, undefined and Strict Null Checks).
  • useUnknownInCatchVariables: the error in a catch block has the type unknown.
  • strictFunctionTypes, strictBindCallApply, noImplicitThis, strictPropertyInitialization and strictBuiltinIteratorReturn: 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.

Two ways to run meter.ts. Top: tsc type-checks it and emits meter.js, which node runs. Bottom: Node.js, tsx, Deno or Bun strip the types without checking them and run the result directly. compile, then run meter.ts tsc check + emit meter.js node strip types and run meter.ts Node.js, tsx, Deno or Bun removes types, no check runs
Only the top route checks types. With the bottom route, run 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 enum and constructor parameter properties, are rejected (see Enums, const Assertions and satisfies). The compiler option erasableSyntaxOnly makes tsc report them in advance.
  • tsconfig.json is ignored, and imports must name the real file, including its extension: import { DAY } from "./tariffs.ts". Type-only imports must be written as import 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, or npx tsx watch meter.ts to restart on every change.
  • Deno and Bun, the two alternative JavaScript runtimes, run .ts files 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 using tsc for 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.