skip to content

TypeScript Installation & Running

Install the TypeScript compiler, run .ts files without a build step using ts-node or tsx, and compile projects with tsc. Covers tsc flags, watch mode, and project references.

16 min read 77 snippets deep dive

TypeScript Installation & Running#

What it is#

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. The official tsc compiler performs type-checking and emits JavaScript output. Several faster alternatives — ts-node, tsx, and Bun — allow running TypeScript files directly without a separate compile step, which is useful during development.

Install tsc#

TypeScript is installed as a dev dependency per-project. Avoid global installs so each project pins its own version.

npm install -D typescript

Verify the installation:

npx tsc --version

Output:

Version 5.4.5

Generate a starter tsconfig.json:

npx tsc --init

Output:

Created a new tsconfig.json with:
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true

Run TypeScript without a compile step#

ts-node#

ts-node is the classic Node.js TypeScript runner. It compiles files in-memory and executes them with Node.

npm install -D ts-node
npx ts-node src/index.ts

For ESM projects, use the --esm flag or the ts-node/esm loader:

npx ts-node --esm src/index.ts
# or
node --loader ts-node/esm src/index.ts

tsx#

tsx is a fast, ESM-compatible TypeScript runner built on esbuild. It starts faster than ts-node and handles .ts, .tsx, .mts, and .cts files transparently.

npm install -D tsx
npx tsx src/index.ts

Watch mode (re-runs on file changes):

npx tsx watch src/index.ts

Using as a Node.js loader (for programmatic use or legacy flags):

node --import tsx/esm src/index.ts

Bun#

Bun natively strips TypeScript types and executes the file directly — no separate package is needed.

bun run src/index.ts

Output:

Hello from TypeScript

[!TIP] For new projects, tsx is the recommended dev-time runner in 2026. It is faster than ts-node, supports both CJS and ESM, and requires no extra configuration.

Compile with tsc#

Basic compile#

Compiles all files listed in tsconfig.json and emits JavaScript to the configured outDir:

npx tsc

Watch mode#

Re-compiles whenever a source file changes:

npx tsc --watch

Output:

[12:00:00] Starting compilation in watch mode...
[12:00:01] Found 0 errors. Watching for file changes.

Type-check only (no emit)#

Validates types without writing any output files — useful in CI:

npx tsc --noEmit

Output (no errors):

(no output — exit code 0)

Output (with errors):

src/index.ts:5:10 - error TS2322: Type 'string' is not assignable to type 'number'.

5   const n: number = "hello";
              ~~~~~~~~~~~~~~~
Found 1 error.

Override output directory#

npx tsc --outDir dist

Compile a single file (bypasses tsconfig)#

npx tsc src/utils.ts --target ES2022 --module ESNext

[!WARNING] Passing individual file names to tsc disables tsconfig.json. All options must be specified via CLI flags when doing this.

Compile with source maps#

npx tsc --sourceMap

This emits a .js.map file alongside each .js file, enabling debuggers to map back to the original TypeScript source.

Project references#

Large monorepos split TypeScript code into multiple sub-projects. tsc --build (alias tsc -b) compiles them in dependency order and caches results.

npx tsc --build
npx tsc --build --watch       # watch all referenced projects
npx tsc --build --clean       # delete all build outputs
npx tsc --build --force       # rebuild even if up to date

A root tsconfig.json referencing sub-projects:

{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/api" },
    { "path": "./packages/ui" }
  ]
}

Each referenced package’s tsconfig.json must include "composite": true:

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

Useful tsc flags#

FlagDescription
--noEmitType-check only, no output files
--watch / -wRecompile on change
--build / -bIncremental project-reference build
--strictEnable all strict checks
--target <ES>Set output JS version
--module <fmt>Set module format (commonjs, esnext, nodenext)
--outDir <dir>Output directory
--sourceMapEmit .js.map files
--declarationEmit .d.ts files
--diagnosticsPrint timing diagnostics
--listFilesPrint files included in the compilation

Choosing a runner#

ToolSpeedESMCJSType errors shownNotes
tscSlowest✅ (full)Official, required for .d.ts emit
ts-nodeSlowPartialMature, widely supported
tsxFast❌ (type-strip only)Recommended for dev
bun runFastest❌ (type-strip only)No Node.js compatibility layer

[!NOTE] tsx and bun do not run the TypeScript type checker — they only strip types. Always run tsc --noEmit in CI to catch type errors.

TypeScript versions and release cadence#

TypeScript ships a minor version roughly every three months. Each minor (5.0, 5.1, 5.2, …) introduces new language features and may include breaking changes; patches are bug-fix only. The version installed in your project’s devDependencies is the version that governs every editor, every CI run, and every contributor’s compiler.

npm view typescript versions --json | tail -20

Output:

[
  "5.3.0",
  "5.3.2",
  "5.3.3",
  "5.4.0",
  "5.4.2",
  "5.4.3",
  "5.4.4",
  "5.4.5",
  "5.5.0",
  "5.5.2",
  "5.5.3",
  "5.6.0",
  "5.6.2",
  "5.7.2"
]

Pin to a specific minor in package.json to make builds deterministic. The ~ semver range lets the patch float (5.4.55.4.x) without accidentally moving to 5.5, where syntax may change:

{
  "devDependencies": {
    "typescript": "~5.4.5"
  }
}

Check the current install matches the lockfile:

npm ls typescript

Output:

my-project@1.0.0 /repo
└── typescript@5.4.5

Upgrade to the next minor in a controlled way — bump the package, run tsc --noEmit, then commit:

npm install -D typescript@5.5
npx tsc --noEmit

Output:

src/legacy.ts:12:5 - error TS2322: Type 'undefined' is not assignable to type 'string'.

12     const v: string = maybeUndefined();
       ~~~~~~~~~~~~~~~~

Found 1 error.

Every new minor often surfaces additional errors as the type-checker gets stricter. Read the release notes for breaking-change call-outs before upgrading.

Global vs local tsc#

tsc installed globally (npm install -g typescript) puts a single binary on $PATH that ignores your project’s version. The risk: contributor A’s global is 5.0 and emits one shape of output; contributor B’s global is 5.7 and emits another. Lock everything to the local install via npx tsc (or pnpm tsc, bun tsc, yarn tsc), which resolves from node_modules/.bin first.

which tsc
npx tsc --version

Output:

/Users/alice/.nvm/versions/node/v22.10.0/bin/tsc
Version 5.4.5

Notice which tsc finds a global binary first, but npx tsc runs the project’s node_modules/.bin/tsc. Always invoke via npx/pnpm exec/bun x so the lockfile rules.

For a project where npx is annoying, add an npm script that aliases tsc to the local binary:

{
  "scripts": {
    "tsc": "tsc",
    "typecheck": "tsc --noEmit",
    "build": "tsc"
  }
}
npm run typecheck

Output:

> tsc --noEmit
(no output — exit code 0)

The shell script form (npm run …) prepends node_modules/.bin to $PATH automatically — every binary listed in any installed package becomes callable from a script without npx.

Choosing a TypeScript distribution#

Most projects use the official typescript package from npm, but several alternatives ship faster compilers or different feature sets. They are not drop-in replacements — each has trade-offs.

DistributionWhat it providesWhen to reach for it
typescript (official)tsc, tsserver (language service), tsc --buildDefault — required for .d.ts emit and full type-checking.
@swc/core + @swc/cliRust-based transpiler (no type-check)CI builds where speed matters more than checking.
esbuildGo-based transpiler (no type-check)Bundler + transpile in one binary; can --bundle too.
@types/typescriptType declarations only — used inside the TS source treeNot user-facing; ignore.
tsc-watchWrapper around tsc -w with hooksWhen you want to run a script after every successful compile.
ttsc (ttypescript)tsc + transformer plugin hooksLegacy — superseded by SWC plugins.

The recommendation in 2026: use the official typescript package for type-checking and .d.ts emit, and use a bundler (Vite, esbuild, Bun) for the actual JS output. Do not try to make swc or esbuild replace tsc — they do not type-check.

npm install -D typescript @swc/core

Output: (none — exits 0 on success)

devDependencies layout for a TypeScript project#

A modern TypeScript project’s package.json typically lists these dev dependencies. Each entry is here for a reason; deleting one silently breaks a workflow.

{
  "name": "my-app",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "lint": "eslint src"
  },
  "devDependencies": {
    "typescript": "~5.4.5",
    "tsx": "^4.19.0",
    "@types/node": "^22.5.0",
    "vitest": "^2.0.0",
    "eslint": "^9.0.0",
    "@typescript-eslint/parser": "^8.0.0",
    "@typescript-eslint/eslint-plugin": "^8.0.0"
  }
}

The standard four:

  • typescript — the compiler itself.
  • tsx — fast dev-time runner (replaces ts-node).
  • @types/node — type declarations for Node’s stdlib (fs, path, process, etc.). Required for any Node script.
  • @typescript-eslint/* — TypeScript-aware ESLint rules.
npm install -D typescript tsx @types/node

Output:

added 3 packages in 1.2s

@types/* packages#

Most npm packages either ship their own .d.ts files (bundled types) or have a community-maintained @types/<name> package on the @types scope. The TypeScript compiler auto-discovers any @types/* package in node_modules — you don’t need to register them anywhere.

npm install -D @types/node @types/express @types/lodash

Output:

added 3 packages in 0.8s

For a library that doesn’t have @types/<name> and doesn’t bundle types, TypeScript errors with TS7016. The fix is either to author a one-line ambient declaration (see .d.ts files) or to install the community types if they exist:

npm install -D @types/some-library

Output: (none — exits 0 on success)

A handful of types packages are pinned to specific runtime versions — @types/node@22.x for Node 22, @types/node@20.x for Node 20. Match the runtime you’re targeting, not the latest:

node --version
npm install -D @types/node@22

Output:

v22.10.0
added 1 package in 0.4s

Editor integration#

Every IDE that supports TypeScript reads from the same tsserver binary inside node_modules/typescript/lib/tsserver.js. The editor and tsc see the same world if (and only if) they use the same version.

VS Code workspace TypeScript version#

VS Code ships its own bundled TypeScript and uses it by default. For a per-project version, click the version indicator in the status bar (bottom-right when a .ts file is open) and pick “Use Workspace Version”. The selection is saved to .vscode/settings.json:

{
  "typescript.tsdk": "node_modules/typescript/lib"
}

This ensures every developer who opens the project sees the same compiler. Commit this setting.

Neovim / LSP#

typescript-language-server (or vtsls, a newer fork) wraps tsserver and exposes it over LSP. Pick the project’s TypeScript by setting the server’s tsdk option:

require('lspconfig').vtsls.setup({
  settings = {
    typescript = {
      tsdk = vim.fn.getcwd() .. '/node_modules/typescript/lib',
    },
  },
})

Refresh the editor after upgrading#

After npm install -D typescript@<new-version>, the running editor still has the old tsserver in memory. VS Code: Command Palette → TypeScript: Restart TS Server. Neovim: :LspRestart.

# To force every editor's restart on macOS, touch the project's tsconfig:
touch tsconfig.json

Output: (none — exits 0 on success)

Cross-runtime: TypeScript outside Node.js#

Node is the most common host for TypeScript, but several other runtimes treat TypeScript as a first-class input. Each handles the type-strip / type-check split differently — none of these replaces tsc for type-checking. See ts-node, tsx & friends for the deep comparison.

RuntimeTS supportType-checks?Notes
Node 22.6+--experimental-strip-types flagNoBundled stripper based on amaro. Default-on in Node 23.6+.
BunNative parserNoSingle binary, fastest cold start.
DenoNative via swcYes (by default)Sandboxed; type-check toggleable.
Cloudflare WorkersWrangler/esbuildNoBuild step happens in the deploy pipeline.
VercelesbuildNoSame — TS handled by the platform’s bundler.

Use the right tool for the job, but always run tsc --noEmit somewhere in the chain. Stripping is not checking.

node --experimental-strip-types src/index.ts
bun src/index.ts
deno run --allow-net src/index.ts

Output:

Hello from TypeScript

What tsc actually does, end-to-end#

Knowing what happens between npx tsc and the bytes hitting your dist/ directory makes diagnosing build problems much easier. The compiler runs five passes:

  1. Locate inputs — read tsconfig.json, expand include/exclude/files, plus every transitive import.
  2. Parse — produce AST nodes for every .ts / .tsx / .d.ts file.
  3. Bind — assign each declaration a symbol and link references back to declarations.
  4. Check — run the type-checker pass-by-pass, surfacing errors.
  5. Emit — write .js, .d.ts, and .map files according to compilerOptions.

You can interrupt the pipeline at each stage with a flag. --noEmit stops after pass 4. --declaration --emitDeclarationOnly writes only .d.ts and skips .js. --listFiles prints every file scanned during pass 1.

npx tsc --listFiles --noEmit | head -10

Output:

/repo/node_modules/typescript/lib/lib.es5.d.ts
/repo/node_modules/typescript/lib/lib.es2015.d.ts
/repo/node_modules/typescript/lib/lib.es2016.d.ts
/repo/node_modules/typescript/lib/lib.dom.d.ts
/repo/node_modules/@types/node/index.d.ts
/repo/src/index.ts
/repo/src/util.ts

--diagnostics prints timing breakdown for each pass:

npx tsc --diagnostics --noEmit

Output:

Files:                          47
Lines:                       18421
Identifiers:                 36842
Symbols:                     12387
Types:                        4192
Parse time:                   0.42s
Bind time:                    0.12s
Check time:                   1.87s
Emit time:                    0.00s
Total time:                   2.41s

When a build feels slow, this is where to look. Long bind time points to too many imports; long check time points to gnarly generic types. The compiler API exposes the same numbers programmatically via ts.performance.

Comparison with Python and other ecosystems#

TypeScript installation differs from typed languages in adjacent ecosystems. The mental model “TypeScript = Python’s mypy” is useful but incomplete.

ConcernTypeScriptPython (mypy / pyright)Rust
Compiler ships in stdlib?No — install typescript from npmNo — install mypy from PyPIYes — rustc is the language
Per-project version pin?devDependenciespyproject.toml / requirements.txtrust-toolchain.toml
Build artifact required?Optional — .ts.js for prodType-check only; runs .py directlyAlways — cargo build → binary
Editor uses local install?Yes via tsserverMostly — pyright uses its own bundled copyrust-analyzer reads rust-toolchain.toml
Strict by default?Opt-in via strict: trueOpt-in via strict = true in pyproject.tomlAlways

The most surprising-for-newcomers part of TypeScript: there is no runtime — the type system disappears. Compare with Rust where the compiler enforces types end-to-end, or Python where mypy is purely advisory but the runtime exists.

Common pitfalls#

  1. Calling a global tsc by mistakewhich tsc resolves to the global, but the project’s version is in node_modules. Always use npx tsc or an npm script.
  2. @types/node version doesn’t match runtimeprocess.env.X typed as string | undefined even after augmenting ProcessEnv if @types/node is from a different major. Pin it to the Node major you run.
  3. Mixing dev runners — using ts-node in dev, tsc in build, and an editor that picks a different bundled TypeScript. The three should agree on tsconfig.json; pick one TypeScript version everywhere.
  4. npm install -g typescript only — works, until a contributor’s global is a different minor. Always install locally and reference via npx.
  5. VS Code says “Use Workspace Version” but you ignore the prompt — VS Code’s bundled TypeScript is older than the project’s, so the editor flags errors the build doesn’t. Always commit .vscode/settings.json with typescript.tsdk.
  6. tsx not installed in CI — your dev runs fine because tsx is in node_modules, but CI uses npm ci --omit=dev and skips it. Move tsx from devDependencies to wherever the CI script needs it, or run the build before --omit=dev.
  7. bun run reading the wrong package script — Bun runs package.json scripts but ignores pre*/post* hooks. If your prebuild step is critical, call it explicitly.
  8. Mixing tsx and node shebangs — the file’s first line decides who runs it. #!/usr/bin/env -S npx tsx works for the author but fails for users without tsx installed. Build to JS for distribution.
  9. emitDeclarationOnly without declaration: truetsc silently emits nothing. Always pair them.
  10. Stale .tsbuildinfo after an upgradetsc may skip recompilation believing nothing changed. Run tsc -b --force once after upgrading the compiler.

Real-world recipes#

Pin TypeScript across a monorepo#

Every workspace inherits the root typescript install via npm/pnpm/Bun hoisting. Pin in one place; never re-install at a child workspace.

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "typescript": "~5.4.5",
    "tsx": "^4.19.0"
  }
}
pnpm install
pnpm -r exec tsc --version

Output:

packages/shared  | Version 5.4.5
packages/ui      | Version 5.4.5
packages/app     | Version 5.4.5

Migrate from ts-node to tsx#

The standard 2026 dev runner migration — same package.json shape, two-line diff.

npm uninstall ts-node
npm install -D tsx

Output:

removed 14 packages in 0.5s
added 1 package in 0.3s

Update package.json:

{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "start": "node dist/server.js"
  }
}
npm run dev

Output:

[tsx] watching src/server.ts
Server ready on http://localhost:3000

If you used ts-node’s register hook for tests (mocha -r ts-node/register), swap to tsx (mocha --import tsx).

Type-check in CI without emitting#

The single most common CI script: type-check the project, fail the build on errors, write nothing to disk.

npx tsc --noEmit

Output:

(no output — exit code 0)

For monorepos with project references, use tsc -b --noEmit instead — it walks the reference graph and skips up-to-date sub-projects.

npx tsc -b --noEmit

Output:

(no output — exit code 0)

Install TypeScript without Node (Bun-first project)#

If your project never touches Node — say, a Cloudflare Worker built with Bun — TypeScript still installs via Bun’s package manager. Bun reads package.json and writes a bun.lock:

bun add -d typescript @types/bun

Output:

bun add v1.2.18

 installed typescript@5.4.5
 installed @types/bun@1.1.5

 2 packages installed [123.00ms]

The @types/bun package provides TypeScript declarations for the Bun.* namespace, bun:test, and Bun’s bundler API. With it in devDependencies, your editor sees Bun.serve(), Bun.file(), and import { describe } from "bun:test" without errors.

Add TypeScript to an existing JavaScript project#

Three commands turn a plain JS repository into a TypeScript-aware one — no rewrite required. allowJs: true lets tsc type-check existing .js files; you migrate file-by-file.

npm install -D typescript @types/node
npx tsc --init

Output:

Created a new tsconfig.json with:
  target: es2016
  module: commonjs
  strict: true
  esModuleInterop: true
  skipLibCheck: true
  forceConsistentCasingInFileNames: true

Edit tsconfig.json to enable JavaScript checking:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "allowJs": true,
    "checkJs": false,
    "noEmit": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}
npx tsc --noEmit

Output:

(no output — exit code 0)

Now rename one file at a time from .js to .ts and watch the type errors appear. See project references for surgical per-directory migration.

Reproduce the project compiler version exactly#

When tracking down “works on my machine” type errors, compare the TypeScript version in use across machines.

npx tsc --version
npm ls typescript --json | jq -r '.dependencies.typescript.version'

Output:

Version 5.4.5
5.4.5

If those two disagree, an editor or a script is using a different version. Force the local version via npx, restart the editor, and confirm again.