Question
When to Put @types Packages in dependencies vs devDependencies in TypeScript
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/*indevDependenciesfor normal applications - Put
@types/*independenciesonly when downstream users need them to use your published package correctly
Why the distinction exists
In package.json:
dependenciesare packages required when the project is installed and used normallydevDependenciesare 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:
dependenciesare the things you must bring on the trip because you will actually use them there.devDependenciesare 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.
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:
expressis needed at runtime@types/expressis 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
-
You install packages
expressprovides the real runtime code.@types/expressprovides type information.
-
You write TypeScript code
- The TypeScript compiler reads the type definitions from
@types/express. - Your editor can autocomplete
app.get,req, andres.
- The TypeScript compiler reads the type definitions from
-
You compile the code
- TypeScript checks that your code is valid.
- The output JavaScript does not contain
@types/express.
-
You run the compiled JavaScript
- Node.js uses
express. - Node.js does not use
@types/expressat runtime.
- Node.js uses
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 indevDependencies
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.
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:
typescriptts-nodeeslintjestvitest@types/jest@types/nodein 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
| Situation | Put @types/* in devDependencies? | Put @types/* in dependencies? | Why |
|---|---|---|---|
| Private app | Yes, usually | Rarely | Types are only needed during development/build |
| Backend server | Yes, usually | Rarely | Runtime uses JS package, not type package |
| Frontend app | Yes, usually | Rarely | Build tools use the types, browser does not |
| Published library with internal-only types | Yes, often | Sometimes not needed | Consumers do not depend on those types directly |
| Published library with exported external types | Sometimes no | Yes, often | Consumers may need those types to use your API |
dependencies vs devDependencies
| Field | Purpose | Installed for production? | Typical examples |
|---|---|---|---|
dependencies | Needed for normal use of the package | Yes | express, react, lodash |
devDependencies | Needed only for development, testing, building | Usually no in production installs | typescript, eslint, jest, @types/node |
Quick comparison
- Application project: prefer
devDependencies - Published library: use
dependenciesonly if your consumers need those types
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/*indevDependencies. - For published libraries, put
@types/*independenciesonly 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/*→ maybedependencies
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.
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
- Create a small TypeScript app that uses
express. - Add the necessary type packages and place them in the correct section of
package.json. - Create a small reusable TypeScript library that exports a type or function based on
expresstypes. - Update the library
package.jsonso its type-related dependencies match its public API. - Verify that both projects can type-check successfully.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.