npm typescript: Install, Pin, Run
Every TypeScript project starts the same way: npm install typescript. One command, a few
seconds, done. The part nobody explains is what that command actually puts on your machine,
why the answer to "should I install it globally?" is almost always no, and why two
developers on the same repo can end up compiling with different versions of the compiler.
This page covers the npm typescript setup end to end — the install itself, how npx tsc
resolves, what @types/* packages are for, how to pin a version so your CI and your laptop
agree, and the four mistakes that produce most setup bug reports.
What npm typescript Actually Installs
The typescript package on npm is the compiler. It is not a runtime, not a plugin, and not
something your shipped code depends on. It ships two binaries and a pile of .d.ts files
describing the JavaScript standard library.
[object Object]After that finishes you have:
| What | Where | Does what |
|---|---|---|
tsc | node_modules/.bin/tsc | Type-checks and emits JavaScript |
tsserver | node_modules/.bin/tsserver | The language server your editor talks to |
lib.*.d.ts | node_modules/typescript/lib/ | Types for Array, Promise, the DOM |
A devDependencies entry | package.json | Pins the version for everyone else |
That last row is the one that matters most, and it is the reason for the --save-dev flag.
TypeScript disappears at build time — the output is plain JavaScript — so it belongs in
devDependencies, not dependencies. Putting it in dependencies means every production
install downloads a compiler it will never run.
The tsserver row is worth a second look too. The autocomplete and red squiggles in your
editor come from that binary, and most editors prefer the one in your node_modules over
their bundled copy. Install TypeScript and your editor quietly starts using the same version
your build uses. That is the whole reason local installs win.
Local or Global: Install It Locally
You will find plenty of tutorials that open with npm install -g typescript. Skip that.
A global install gives you one compiler version for every project on the machine. That is
fine until you have two projects — one on 5.4, one on 5.9 — and the global tsc silently
checks both with whichever it happens to be. Errors appear in one place and not the other,
nobody can reproduce anything, and the difference never shows up in a diff.
# ❌ One compiler for every project you will ever open
npm install -g typescript
# ✅ One compiler per project, recorded in package.json
npm install --save-dev typescriptThe local install is also the only one your teammates inherit. npm install on a fresh
clone pulls the exact compiler the repo expects; a global install lives on your machine and
nowhere else, so CI gets whatever its base image happens to carry.
There is one honest exception. If you want tsc available for throwaway files outside any
project, a global install is convenient. Keep it, but keep a local one in every real project
too — the local copy always wins inside its own directory.
Running the Compiler with npx
Once TypeScript is a dev dependency, you do not call tsc directly. You call it through
npx, which looks in node_modules/.bin before it looks anywhere else:
npx tsc --version
npx tsc --init
npx tscnpx tsc --init writes a tsconfig.json with the defaults and a long list of commented-out
options. That file is where the real decisions live — strict, target, module — and the
tsconfig.json page walks through the ones that change something
you will notice.
Make a file and check that the whole chain works:
const compilerCheck: string = 'tsc is wired up'
console.log(compilerCheck.toUpperCase())Run npx tsc and you get a .js file next to it. If you get tsc: command not found
instead, the install did not land in this project — check that node_modules exists and
that typescript is in your package.json.
The habit worth building is putting the commands in scripts so nobody has to remember the
flags:
{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "tsc"
}
}npm run typecheck now does the same thing on every machine, including the CI runner.
--noEmit checks types without writing output, which is what you usually want when a
bundler is already handling the JavaScript.
Pinning the Version
By default npm writes a caret range:
{
"devDependencies": {
"typescript": "^5.9.2"
}
}For most packages a caret is fine. For TypeScript it is a little sharper than it looks, because TypeScript does not follow semver the way libraries do. A minor release can add a check that flags code which compiled cleanly yesterday. Nothing broke at runtime — the compiler simply got better at noticing something — but your build is red and you did not change a line.
Two reasonable positions. Pin exactly and upgrade on purpose:
{
"devDependencies": {
"typescript": "5.9.2"
}
}Or keep the caret and rely on your lockfile, which already records the exact version every
npm ci installs. The lockfile is the real guarantee either way; an exact range just makes
the intent visible in the file people actually read.
What you should not do is leave TypeScript out of the lockfile's reach — npm install -g in
a Dockerfile, or a CI step that installs the latest before building. That is how a green
pipeline turns red on a day nobody committed anything. The
TypeScript 5.9 write-up is a good example of what a single
minor bump can change.
The @types Packages
TypeScript knows about JavaScript's built-ins and nothing else. Anything that came from
outside the language — Node's process, a library written in plain JavaScript — needs type
definitions, and that is what the @types scope on npm is for.
[object Object]With that installed, Node's globals are typed:
const serverPort = process.env.PORT ?? '3000'
console.log(`listening on ${serverPort}`)Without it, the compiler has never heard of process and tells you so.
Three rules cover almost every case:
- A library written in TypeScript ships its own types. Nothing to install. If
package.jsonhas atypesorexportsfield pointing at.d.tsfiles, you are done. - A JavaScript library usually has a community package at
@types/<name>. Install it as a dev dependency alongside the library itself. - Anything with no types at all needs a declaration you write yourself:
declare module 'untyped-legacy-lib' {
export function parse(input: string): unknown
}Put that in a .d.ts file inside your project and the import type-checks. unknown rather
than any is deliberate — it forces a narrowing step at the call site instead of handing you
a value the compiler has stopped thinking about.
Four Ways the Setup Goes Wrong
Two TypeScript versions in one repo. A monorepo package pulls its own copy, a global
install shadows it, and your editor picks a third. npm ls typescript prints the whole tree
and usually ends the argument in one line.
TypeScript in dependencies. It works, so nobody notices, and every production install
pays for a compiler it never runs. Move it to devDependencies.
@types packages drifting from the library. @types/node for Node 18 against Node 22
will miss APIs that exist and type ones that do not. Bump them together.
No tsconfig.json. Without one, tsc falls back to loose defaults — no strict, an
old target, CommonJS output — and you spend a week wondering why TypeScript is not catching
anything. Run npx tsc --init on day one. The
ESM page covers the module setting that trips people up next.
Upgrading Without Breaking the Build
Because a minor release can add checks, treat a TypeScript bump as its own change rather than something that rides along with an unrelated pull request:
npm install --save-dev typescript@latest
npm run typecheckIf new errors appear, read them before reaching for a flag to turn them off. In most cases the compiler found something real that older versions let through — a nullable value used without a check, a narrowing that never actually narrowed. Fixing those is the point of the upgrade.
When the errors are genuinely not worth it right now, the escape hatch is to stay on the
previous version for one more cycle rather than to loosen tsconfig.json. A pinned older
compiler is a note to come back; a disabled strict flag is a change that outlives everyone
who remembers why it happened.
One more thing worth doing after any upgrade: restart the TypeScript server in your editor.
It holds the old tsserver in memory, so until you do, your squiggles and your build
disagree and you will chase an error that no longer exists.
Where to Go From Here
The install is the easy part. Local, in devDependencies, run through npx, version
visible in package.json — that covers the npm typescript setup for every project shape
worth having, from a single script to a monorepo.
What comes after is the configuration and the language. The tsconfig.json options decide how strict the compiler is and what it emits. After that, generics and the utility types are where TypeScript starts paying for itself, and the best-practices post collects the settings worth turning on once everything compiles.
Become a TypeScript Pro
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges