Question

When to Put @types Packages in dependencies vs devDependencies in TypeScript

typescriptnpmtypescript-typingspackage.json

Question

I am using TypeScript in a project and want to use a JavaScript library along with its type definitions from @types/*.

For example, I can install the types with:

npm install @types/some-library

I am unsure whether these packages should be saved in dependencies or devDependencies.

My assumption is that @types/* packages usually belong in devDependencies because they are needed during development and compilation, not at runtime. However, I often see @types/* listed under dependencies in real projects, which makes this confusing.

How should I decide whether an @types/* package belongs in dependencies or devDependencies? Are there any commonly accepted or official guidelines for this?

Short Answer

By the end of this page, you will understand what @types/* packages are, why they usually do not affect runtime, and how to choose between dependencies and devDependencies based on whether your project is an application or a published library. You will also learn the common exceptions and mistakes developers make when managing TypeScript type packages.

Concept

@types/* packages provide TypeScript type definitions for JavaScript libraries that do not ship their own types.

For example, if a library is written in plain JavaScript, TypeScript cannot automatically know the shapes of its functions, objects, or classes. An @types/* package fills in that missing information so your editor and compiler can understand how to use the library safely.

Why this matters

Types help with:

  • autocomplete in editors
  • compile-time error checking
  • clearer APIs
  • safer refactoring

But an important detail is this:

  • Type definitions are usually used only during development/build time
  • They are usually not needed when your code runs in production

That is why @types/* packages often go into devDependencies.

The main rule

A good beginner rule is:

  • Put @types/* in devDependencies for normal applications
  • Put @types/* in dependencies only when downstream users need them to use your published package correctly

Why the distinction exists

In package.json:

  • dependencies are packages required when the project is installed and used normally
  • devDependencies are packages needed only for development, testing, building, or type-checking

If your app is a private web app, API server, CLI, or internal tool, the type packages are almost always part of development only.

If you publish a TypeScript library and your exported public API depends on external types from @types/*, consumers may need those types too. In that case, keeping them in dependencies can be appropriate.

Modern note

Many libraries now include their own types directly, so you may not need an @types/* package at all. Always check whether the main package already ships TypeScript definitions before installing @types/*.

Mental Model

Think of runtime code and type definitions as two different kinds of luggage:

  • dependencies are the things you must bring on the trip because you will actually use them there.
  • devDependencies are the planning tools you use before the trip, like a map or checklist.

@types/* packages are usually like the map: they help you prepare, but they are not something the running JavaScript program uses directly.

There is one exception: if you are packing for someone else too—meaning you are publishing a library for other developers—then some type packages may need to travel with the package because your users rely on them to understand your API.

Take Quiz

Syntax and Examples

The decision is made in package.json.

Example: app project

If you are building an application, this is the usual setup:

{
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/express": "^5.0.0"
  }
}

Here:

  • express is needed at runtime
  • @types/express is needed only by TypeScript during development/build

Example: install as a dev dependency

npm install express
npm install -D typescript @types/express

Example: published library

Suppose you publish a TypeScript library and your public API exposes express types:

import { RequestHandler } from "express";

export function createMiddleware(): RequestHandler {
  return (req, res, next) => {
    next();
  };
}

If consumers of your package need those types to type-check your exported API, then related type information may need to be available when they install your package.

A simplified package.json might look like this:

{
  "name": "my-middleware-lib",
  "dependencies": {
    "express": "^4.19.0",
    "@types/express": "^5.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Practical shortcut

Use this quick test:

  • If the package is only for your editor/compiler → devDependencies
  • If people installing your package need it for correct usage of your exported types → dependencies

Step by Step Execution

Consider this small TypeScript app:

import express from "express";

const app = express();

app.get("/", (req, res) => {
  res.send("Hello");
});

And this package.json:

{
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/express": "^5.0.0"
  }
}

What happens step by step

  1. You install packages

    • express provides the real runtime code.
    • @types/express provides type information.
  2. You write TypeScript code

    • The TypeScript compiler reads the type definitions from @types/express.
    • Your editor can autocomplete app.get, req, and res.
  3. You compile the code

    • TypeScript checks that your code is valid.
    • The output JavaScript does not contain @types/express.
  4. You run the compiled JavaScript

    • Node.js uses express.
    • Node.js does not use @types/express at runtime.

Why this matters

This shows why @types/express is usually a devDependency in an application: it helps before runtime, not during runtime.

Now compare with a library

import { Request, Response } from "express";

export type RouteHandler = (req: Request, res: Response) => void;

If your library publishes this type as part of its public API, users of your library may indirectly depend on those types being available. That is the main reason some libraries place @types/* packages in dependencies.

Real World Use Cases

1. Backend application

A Node.js API using express, cors, or jsonwebtoken may install:

  • runtime packages in dependencies
  • @types/* packages in devDependencies

This is the most common setup.

2. Frontend application

A React or Vite app may use packages that already ship their own types, but older JavaScript-only packages may still require @types/*.

In an app, those type packages are usually development-only.

3. Internal company tool

For scripts, build tools, migrations, and automation tasks, the same rule applies:

  • runtime code in dependencies
  • type-only packages in devDependencies

4. Published npm library

If you publish a library and your .d.ts output or exported API references types from another package, consumers may need those types when they install your package.

That is where dependencies can make sense for type packages.

5. DefinitelyTyped-based ecosystem packages

Some older ecosystems rely heavily on separate @types/* packages. In those cases, maintainers sometimes keep them in dependencies to make installation smoother for package consumers.

Take Quiz

Real Codebase Usage

In real projects, developers usually follow a few patterns.

Pattern 1: Application code

For apps, @types/* almost always goes into devDependencies.

Common example:

{
  "dependencies": {
    "pg": "^8.11.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}

Why:

  • the app runs compiled JavaScript
  • the types are only for local development, CI, and builds

Pattern 2: Public API uses external types

Library authors often check whether exported types reference another package.

Example:

import { IncomingMessage } from "http";

export interface RequestContext {
  req: IncomingMessage;
}

If those external types are part of what consumers see, the package may need those typings available during installation and type-checking.

Pattern 3: Avoid unnecessary @types/*

Before installing @types/*, developers check whether the package already includes its own types:

{
  "dependencies": {
    "axios": "^1.0.0"
  }
}

axios already ships types, so @types/axios is unnecessary.

Pattern 4: Guard against consumer pain

Library maintainers sometimes choose dependencies for type packages when it avoids broken editor support for users. This is less about runtime and more about making the package easier to consume.

Pattern 5: Keep build-only tools in devDependencies

These almost always stay in devDependencies:

  • typescript
  • ts-node
  • eslint
  • jest
  • vitest
  • @types/jest
  • @types/node in most apps

Common Mistakes

Mistake 1: Putting every @types/* package in dependencies

This makes your runtime dependency list larger than necessary.

{
  "dependencies": {
    "express": "^4.19.0",
    "@types/express": "^5.0.0"
  }
}

For a normal app, this is usually unnecessary.

Better

{
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "@types/express": "^5.0.0"
  }
}

Mistake 2: Installing @types/* for a package that already includes types

Broken idea:

npm install axios @types/axios

Many modern packages already include their own type definitions.

Better

Check the package documentation or package.json for built-in types.

Mistake 3: Publishing a library with missing public type dependencies

If your exported .d.ts files refer to types from another package, consumers may get type errors if those dependencies are not available.

Example issue:

import { Request } from "express";

export interface Options {
  req: Request;
}

If your users install your package but do not get the necessary types, their build may fail.

Mistake 4: Confusing runtime imports with type-only usage

Beginners sometimes think that because they wrote an import, the corresponding package must always be a runtime dependency.

But a type package is different from the real implementation package.

  • express → runtime dependency
  • @types/express → usually compile-time only

Mistake 5: Forgetting the project type

Ask this first:

  • Is this an app?
  • Or is this a library that others install?

That answer usually determines the correct location.

Mistake 6: Assuming there is one universal rule

There is a strong convention, but not every package is the same. The exported API of a library can change the decision.

Comparisons

SituationPut @types/* in devDependencies?Put @types/* in dependencies?Why
Private appYes, usuallyRarelyTypes are only needed during development/build
Backend serverYes, usuallyRarelyRuntime uses JS package, not type package
Frontend appYes, usuallyRarelyBuild tools use the types, browser does not
Published library with internal-only typesYes, oftenSometimes not neededConsumers do not depend on those types directly
Published library with exported external typesSometimes noYes, oftenConsumers may need those types to use your API

dependencies vs devDependencies

FieldPurposeInstalled for production?Typical examples
dependenciesNeeded for normal use of the packageYesexpress, react, lodash
devDependenciesNeeded only for development, testing, buildingUsually no in production installstypescript, eslint, jest, @types/node

Quick comparison

  • Application project: prefer devDependencies
  • Published library: use dependencies only if your consumers need those types
Take Quiz

Cheat Sheet

  • @types/* packages provide TypeScript type definitions for JavaScript packages.
  • They are usually used at development/build time, not runtime.
  • For most applications, put @types/* in devDependencies.
  • For published libraries, put @types/* in dependencies only if consumers need them for your exported API.
  • Always check whether the main package already ships its own types.

Common commands

npm install some-library
npm install -D @types/some-library

Rule of thumb

  • App: @types/* → devDependencies
  • Library with public type exposure: @types/* → maybe dependencies

Examples

{
  "dependencies": {
    "express": "^4.19.0"
  },
  "devDependencies": {
    "@types/express": "^5.0.0"
  }
}
{
  "dependencies": {
    "express": "^4.19.0",
    "@types/express": "^5.0.0"
  }
}

Use the second pattern only when publishing a library whose consumers need those types.

Edge case

If a package already includes types, do not install a separate @types/* package unless the docs explicitly say to.

FAQ

Should @types/* usually go in devDependencies?

Yes. For most application projects, @types/* packages belong in devDependencies because they are used during development and compilation, not at runtime.

When should @types/* go in dependencies?

Usually when you publish a library and your consumers need those types because your public API exposes them.

Why do I sometimes see @types/* in dependencies?

Because some packages are libraries, not apps, and their users need those type definitions to type-check correctly.

Are @types/* packages used at runtime?

Normally no. They exist for TypeScript tooling, editor support, and compilation.

What if the package already includes its own types?

Then you usually do not need a separate @types/* package.

Is @types/node also usually a dev dependency?

Yes, in most apps it is a devDependency because it helps TypeScript understand Node.js APIs during development.

Is there an official universal rule?

There is no single rule that fits every package, but the common convention is: apps use devDependencies, published libraries may use dependencies when consumers need the types.

Take Quiz

Mini Project

Description

Create a small TypeScript workspace with two parts: an application and a reusable library. The purpose is to practice deciding where type packages belong and to see why the answer changes depending on whether code is private app code or a published package API.

Goal

Build one app and one library, then place @types/* packages in the correct dependency section for each case.

Requirements

  1. Create a small TypeScript app that uses express.
  2. Add the necessary type packages and place them in the correct section of package.json.
  3. Create a small reusable TypeScript library that exports a type or function based on express types.
  4. Update the library package.json so its type-related dependencies match its public API.
  5. Verify that both projects can type-check successfully.
Take Quiz

Keep learning

Related questions

View more