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,
tsxis the recommended dev-time runner in 2026. It is faster thants-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
tscdisablestsconfig.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#
| Flag | Description |
|---|---|
--noEmit | Type-check only, no output files |
--watch / -w | Recompile on change |
--build / -b | Incremental project-reference build |
--strict | Enable all strict checks |
--target <ES> | Set output JS version |
--module <fmt> | Set module format (commonjs, esnext, nodenext) |
--outDir <dir> | Output directory |
--sourceMap | Emit .js.map files |
--declaration | Emit .d.ts files |
--diagnostics | Print timing diagnostics |
--listFiles | Print files included in the compilation |
Choosing a runner#
| Tool | Speed | ESM | CJS | Type errors shown | Notes |
|---|---|---|---|---|---|
tsc | Slowest | ✅ | ✅ | ✅ (full) | Official, required for .d.ts emit |
ts-node | Slow | Partial | ✅ | ✅ | Mature, widely supported |
tsx | Fast | ✅ | ✅ | ❌ (type-strip only) | Recommended for dev |
bun run | Fastest | ✅ | ✅ | ❌ (type-strip only) | No Node.js compatibility layer |
[!NOTE]
tsxandbundo not run the TypeScript type checker — they only strip types. Always runtsc --noEmitin 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.5 → 5.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.
| Distribution | What it provides | When to reach for it |
|---|---|---|
typescript (official) | tsc, tsserver (language service), tsc --build | Default — required for .d.ts emit and full type-checking. |
@swc/core + @swc/cli | Rust-based transpiler (no type-check) | CI builds where speed matters more than checking. |
esbuild | Go-based transpiler (no type-check) | Bundler + transpile in one binary; can --bundle too. |
@types/typescript | Type declarations only — used inside the TS source tree | Not user-facing; ignore. |
tsc-watch | Wrapper around tsc -w with hooks | When you want to run a script after every successful compile. |
ttsc (ttypescript) | tsc + transformer plugin hooks | Legacy — 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 (replacests-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.
| Runtime | TS support | Type-checks? | Notes |
|---|---|---|---|
| Node 22.6+ | --experimental-strip-types flag | No | Bundled stripper based on amaro. Default-on in Node 23.6+. |
| Bun | Native parser | No | Single binary, fastest cold start. |
| Deno | Native via swc | Yes (by default) | Sandboxed; type-check toggleable. |
| Cloudflare Workers | Wrangler/esbuild | No | Build step happens in the deploy pipeline. |
| Vercel | esbuild | No | Same — 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:
- Locate inputs — read
tsconfig.json, expandinclude/exclude/files, plus every transitive import. - Parse — produce AST nodes for every
.ts/.tsx/.d.tsfile. - Bind — assign each declaration a symbol and link references back to declarations.
- Check — run the type-checker pass-by-pass, surfacing errors.
- Emit — write
.js,.d.ts, and.mapfiles according tocompilerOptions.
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.
| Concern | TypeScript | Python (mypy / pyright) | Rust |
|---|---|---|---|
| Compiler ships in stdlib? | No — install typescript from npm | No — install mypy from PyPI | Yes — rustc is the language |
| Per-project version pin? | devDependencies | pyproject.toml / requirements.txt | rust-toolchain.toml |
| Build artifact required? | Optional — .ts → .js for prod | Type-check only; runs .py directly | Always — cargo build → binary |
| Editor uses local install? | Yes via tsserver | Mostly — pyright uses its own bundled copy | rust-analyzer reads rust-toolchain.toml |
| Strict by default? | Opt-in via strict: true | Opt-in via strict = true in pyproject.toml | Always |
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#
- Calling a global
tscby mistake —which tscresolves to the global, but the project’s version is innode_modules. Always usenpx tscor an npm script. @types/nodeversion doesn’t match runtime —process.env.Xtyped asstring | undefinedeven after augmentingProcessEnvif@types/nodeis from a different major. Pin it to the Node major you run.- Mixing dev runners — using
ts-nodein dev,tscin build, and an editor that picks a different bundled TypeScript. The three should agree ontsconfig.json; pick one TypeScript version everywhere. npm install -g typescriptonly — works, until a contributor’s global is a different minor. Always install locally and reference vianpx.- 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.jsonwithtypescript.tsdk. tsxnot installed in CI — your dev runs fine becausetsxis innode_modules, but CI usesnpm ci --omit=devand skips it. MovetsxfromdevDependenciesto wherever the CI script needs it, or run the build before--omit=dev.bun runreading the wrong package script — Bun runspackage.jsonscripts but ignorespre*/post*hooks. If yourprebuildstep is critical, call it explicitly.- Mixing
tsxandnodeshebangs — the file’s first line decides who runs it.#!/usr/bin/env -S npx tsxworks for the author but fails for users withouttsxinstalled. Build to JS for distribution. emitDeclarationOnlywithoutdeclaration: true—tscsilently emits nothing. Always pair them.- Stale
.tsbuildinfoafter an upgrade —tscmay skip recompilation believing nothing changed. Runtsc -b --forceonce 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.