Skip to main content

How to Use TypeScript with npm

You install a new package, run tsc, and suddenly your build breaks because the package ships with types that expect a different TypeScript version than what's in your project. Sound familiar? Getting TypeScript and npm to play well together involves more than just npm install typescript -- you need to understand version management, tsconfig configuration, and the modern tools that have changed how developers run TypeScript day-to-day.

This guide covers everything from basic setup to the patterns that actually hold up in real projects: installing and updating TypeScript, configuring it properly for Node.js, running TypeScript without a build step, managing type definitions, and resolving version conflicts.

1. Installing TypeScript via npm

You have two installation options:

  1. Global installation: Makes TypeScript available system-wide.

    npm install -g typescript

    This lets you run the TypeScript compiler (tsc) from any directory.

  2. Project-specific installation: Adds TypeScript as a development dependency.

    npm install typescript --save-dev

    This installs TypeScript only for your current project, which keeps compiler versions consistent across your team.

Project-specific installation is almost always the right call. Global installs lead to subtle version mismatches -- your colleague's machine runs TypeScript 5.3, yours runs 5.8, and you spend an afternoon chasing a type error that doesn't exist on their end.

After installing, verify it's working:

npx tsc --version

For init commands and project setup, you'll typically want the project-specific installation, which works well with Convex's TypeScript support.

2. Updating TypeScript Using npm

Running an outdated TypeScript version is one of those things that quietly causes problems -- type definitions drift out of sync, new language features aren't available, and compatibility with other packages gets messy. Here's how to update:

  1. Update global installation:
npm update -g typescript
  1. Update project-specific installation:
npm update typescript
  1. Update to a specific version:
npm install typescript@5.8.2 --save-dev

Check your current version anytime with:

npx tsc --version

Always test after upgrading. Newer TypeScript versions sometimes catch errors that slipped through before, so you might see new failures in code you haven't touched. The TypeScript GitHub repository release notes are worth skimming before you upgrade so you know what to expect.

Check your current versions to stay on top of what's available. Managing TypeScript versions becomes especially important when working with frameworks like Convex that provide end-to-end type safety, where your client and server types need to stay in sync.

3. Configuring TypeScript in a Node.js Project with npm

The right tsconfig depends on what you're building. Most npm projects fall into one of three categories, and each needs a different configuration starting point.

For a Node.js application or API server, your output only runs on your own infrastructure, so you can target modern Node.js directly:

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}

For a library you're publishing to npm, you need declaration: true so consumers get type definitions, and a lower target to avoid forcing consumers onto a specific Node.js version:

{
"compilerOptions": {
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.test.ts"]
}

For a monorepo using npm workspaces, each package also needs "composite": true -- covered in Section 7 below.

If you'd rather start from a battle-tested base, the @tsconfig packages provide community-maintained defaults for specific Node.js versions:

npm install --save-dev @tsconfig/node20
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}

One pairing that trips up a lot of developers: "module": "NodeNext" and "moduleResolution": "NodeNext" must be set together. Use one without the other and you'll get confusing import resolution errors that are hard to trace back to the config. Our tsc --init guide covers individual compiler options in detail if you want to understand what each one does.

When working with Convex, check their docs for specific tsconfig recommendations that ensure proper type generation.

4. Setting Up a TypeScript Development Environment with npm

Getting the devDependencies right matters more than it might seem. TypeScript tooling belongs in devDependencies -- it's not needed at runtime and shouldn't bloat your production install.

Here's the core set for a Node.js project:

npm install --save-dev typescript tsx @types/node rimraf
  • typescript -- the compiler, used for production builds and type checking
  • tsx -- runs .ts files directly during development, ~20ms startup vs ts-node's 500ms+
  • @types/node -- types for Node.js built-in APIs (fs, path, process, etc.)
  • rimraf -- cross-platform rm -rf for the clean script

With those in place, your package.json scripts can cover every stage of the workflow:

{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"clean": "rimraf dist",
"prebuild": "npm run clean",
"type-check": "tsc --noEmit"
}
}

Two things worth noting about this structure. First, prebuild runs automatically before build -- npm's lifecycle hooks mean you don't need to manually chain clean && tsc. Second, type-check is separate from build. In development you often want to skip type checking for speed (that's what tsx is for), and in CI you often want type checking without emitting files. Keeping them separate gives you that flexibility.

For linting, add ESLint with TypeScript support as you would for any TypeScript project. The npm-run-dev-with-package-scripts article has good guidance on structuring these scripts so they compose well across different environments.

5. Running TypeScript Without a Build Step

Choosing how to run TypeScript in development affects which npm packages you need and how your scripts are structured. There are now three viable approaches, each with different npm implications.

tsx (install once, use everywhere)

tsx is an npm package that uses esbuild internally. Install it once as a devDependency and it handles development execution, watch mode, and one-off scripts:

npm install --save-dev tsx
{
"scripts": {
"dev": "tsx watch src/server.ts",
"script": "tsx src/migrate.ts"
}
}

tsx respects your tsconfig.json but doesn't perform type checking -- it just runs your code fast. This is the right tradeoff for development: you want iteration speed, and type errors will surface in your type-check script or CI pipeline anyway.

Native Node.js (no extra npm packages)

Node.js 22.18+ and 23.6+ run TypeScript files natively with no additional npm dependencies:

node src/index.ts

No npm install needed, which makes this attractive for scripts and tooling that you'd rather not add to devDependencies. The limitation is that Node strips types only -- it doesn't support TypeScript-specific syntax like enums or namespaces. If your code uses those, you'll get a runtime error rather than a compile-time one.

Which to reach for

The choice comes down to what's in your codebase and how many dependencies you want:

ScenarioBest choicenpm dependency?
Development server with watchtsx watchYes (tsx)
One-off scripts, simple TS onlynode (22.18+)No
Code using enums or namespacestsxYes (tsx)
CI type checkingtsc --noEmitAlready have typescript
Production buildtscAlready have typescript

If you're already using tsx for your dev server, use it for scripts too -- there's no reason to mix approaches in the same project.

6. Using npm Scripts to Compile TypeScript

Here's what a full script setup looks like once you've got the dev workflow sorted:

{
"scripts": {
"build": "tsc",
"build:prod": "tsc --project tsconfig.prod.json",
"watch": "tsc --watch",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"clean": "rimraf dist",
"prebuild": "npm run clean",
"type-check": "tsc --noEmit",
"lint": "eslint src --ext .ts",
"test": "jest"
}
}

npm run build for production, npm run dev during development. The prebuild lifecycle hook cleans the output directory automatically before every build, so you're never shipping stale compiled files.

For larger projects, a separate tsconfig.prod.json lets you exclude test files from the production build:

{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"]
}

The function compilation can be customized further by modifying compiler options in your tsconfig.json file.

7. Using TypeScript with npm Workspaces

Split your codebase into multiple packages and you've got two coordination problems: keeping node_modules from duplicating across packages, and making sure TypeScript knows how the packages depend on each other. npm workspaces (npm 7+) handles the first problem, and TypeScript project references handle the second.

Set up your workspace in the root package.json:

{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"]
}

Each package needs its own tsconfig.json. Enable composite mode so TypeScript can build dependencies incrementally:

{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}

In your root tsconfig.json, add references to all your packages:

{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/client" }
]
}

Build all packages from the root with:

npx tsc --build

TypeScript builds packages in dependency order and skips anything that hasn't changed. In a larger repository, that's the difference between a 30-second build and a 3-second one.

8. Resolving TypeScript Version Conflicts in npm Projects

You add a new package, run your build, and suddenly get type errors you didn't write. Chances are you've got two different TypeScript versions in your dependency tree and they're fighting each other. Here's how to find out and fix it.

Check your dependency tree first:

npm ls typescript

This prints your full TypeScript dependency tree. If you see the same package at two different versions, that's your conflict.

Install a specific version to pin your project:

npm install typescript@5.8.2 --save-dev

Use package resolutions in package.json to force a specific version across all dependencies (requires npm v7+):

{
"overrides": {
"typescript": "5.8.2"
}
}

Note that npm uses "overrides", not "resolutions" -- the latter is a Yarn feature.

Pin your devDependencies with exact versions to prevent accidental upgrades:

{
"devDependencies": {
"typescript": "5.8.2"
}
}

If you're using Convex's TypeScript integration, check their docs for recommended TypeScript versions. Version conflicts often appear when using multiple packages with @types dependencies, so npm ls typescript is your first stop when a build breaks unexpectedly.

For projects with complex dependency trees, pnpm provides stricter isolation and better conflict resolution than npm by default.

9. Adding TypeScript to an Existing JavaScript Project with npm

The tsconfig changes for migrating a JS project to TypeScript are covered in the tsc --init guide. What that guide doesn't cover is how to update your npm setup -- the package.json changes, the devDependencies you need, and how to roll out the migration without breaking your existing workflow.

Start with the npm side:

npm install --save-dev typescript tsx @types/node

Then update your package.json scripts. You'll probably have something like "start": "node src/index.js" today. The migration target looks like this:

{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"type-check": "tsc --noEmit"
}
}

Don't switch all your scripts at once. Keep the old start script pointing at your JS files until you're confident the TypeScript build produces correct output.

Update .gitignore to exclude the new build output directory:

dist/
*.tsbuildinfo

Add @types packages for any libraries that don't bundle their own types:

# Check what you're using and install @types as needed
npm install --save-dev @types/express @types/lodash

You can check whether a package includes its own types by looking for a "types" or "typings" field in its package.json inside node_modules. If it's missing, search npm for @types/package-name.

Run your first type check once the tsconfig is in place:

npm run type-check

This gives you a baseline of how many errors exist without blocking your build. Fix them file by file. Using utility types like Partial<T> and Pick<T, K> often helps bridge gaps when existing data shapes don't quite match the types you'd write from scratch.

For projects using Convex, their documentation covers TypeScript integration specifically.

10. Adding TypeScript Types to npm Packages

If you publish an npm package without type definitions, TypeScript users are stuck with any or have to write their own declaration files. Neither is a good experience. Here's how to do it right.

Configure your package.json to point to your type definitions:

{
"name": "your-package",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"]
}

The exports field gives you fine-grained control over what consumers can import. It's the modern replacement for just setting "main", and lets you serve different files for ESM vs CJS while keeping types pointed at the right place.

Configure TypeScript to generate declaration files:

{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "./dist"
}
}
  • declaration: Generates .d.ts files alongside your JavaScript output
  • declarationMap: Creates sourcemaps for type definitions so users can jump to source

For dependencies without bundled types, check if an @types package exists:

npm install @types/dependency-name --save-dev

If your package has a peer dependency on TypeScript itself, specify the version range:

{
"peerDependencies": {
"typescript": ">=5.0"
}
}

When working with frameworks like Convex, you'll find they provide built-in type definitions that work directly with your package. Using generics<T> helps create flexible, reusable type definitions for your package's API without forcing consumers into a specific shape.

Final Thoughts on TypeScript and npm

The gap between "TypeScript installed" and "TypeScript working well" comes down to a few decisions: project-specific installs for team consistency, a well-configured tsconfig that actually matches your Node.js version, and choosing the right tool for each stage of your workflow (tsc for production, tsx for development, tsc --noEmit for CI type checks).

The ecosystem has also shifted. If you're still reaching for ts-node by default, it's worth trying tsx -- the speed difference in development is real. And if you're on Node.js 22.18+, running simple scripts directly with node script.ts is now a genuine option.

Keep these in mind:

  • Use project-specific installations to keep TypeScript versions consistent across the team
  • Pair "module": "NodeNext" with "moduleResolution": "NodeNext" in tsconfig
  • Use tsx for development, tsc for production builds, and tsc --noEmit in CI
  • Use npm overrides (not resolutions) to resolve version conflicts in npm projects
  • Ship .d.ts files with any package you publish to npm

The TypeScript documentation and Convex's TypeScript resources are good next stops when you're ready to go deeper.