npm typescript: Install, Pin, Run

October 3, 20268 min read
Requirements:
FunctionsObjects

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:

WhatWhereDoes what
tscnode_modules/.bin/tscType-checks and emits JavaScript
tsservernode_modules/.bin/tsserverThe language server your editor talks to
lib.*.d.tsnode_modules/typescript/lib/Types for Array, Promise, the DOM
A devDependencies entrypackage.jsonPins 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 typescript

The 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 tsc

npx 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:

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 typecheck

If 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.

Share this article

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

Practice with Challenges

Put your npm typescript: install, pin, run knowledge to the test with these related challenges.

#4Pick
Easy
#3312Parameters
Easy

Related Concepts

Concepts that build on or relate to npm typescript: install, pin, run.

tsconfig.json ExplainedWhat Is ESM? ES Modules ExplainedTypeScript Utility TypesTypeScript GenericsWhat Is a .tsx File?