Avatar ·

Как правильно размещать @types/* пакеты: в dependencies или devDependencies?

Привет! Я новичок в TypeScript и пытаюсь разобраться с настройкой проекта. У меня есть небольшое приложение на Express, и я установил типы для express, node и некоторых других пакетов. Вот мой package.json (часть): ```json { "dependencies": { "express": "^4.18.2", "@types/express": "^4.17.21", "@types/node": "^20.11.5", "lodash": "^4.17.21", "@types/lodash": "^4.17.0" }, "devDependencies": { "typescript": "^5.3.3", "ts-node": "^10.9.2", "@types/jest": "^29.5.11" } } ``` Я заметил, что в некоторых проектах @types/* пакеты кладут в devDependencies, а в других — в dependencies. Например, @types/express я положил в dependencies, потому что он нужен для работы самого express. А @types/jest — в devDependencies, так как jest используется только для тестов. Но потом я прочитал, что TypeScript типы — это вообще не runtime-зависимости, и их нужно всегда класть в devDependencies. Теперь я запутался: что делать с @types/express, @types/node и @types/lodash? Они же напрямую не используются в продакшене, но без них код не скомпилируется при установке пакетов пользователем (если это библиотека, а не приложение). Может быть, есть какое-то правило? Или это зависит от того, публикую ли я библиотеку или пишу приложение? Как правильно организовать зависимости в моём проекте? Буду благодарен за подробное объяснение с примерами! Я хочу понять принцип, а не просто запомнить, куда что класть.<<>>

Ключевое правило: приложение vs библиотека

Главный принцип, который нужно усвоить: @types/* пакеты почти всегда должны быть в devDependencies, за исключением случая, когда вы публикуете библиотеку, и её пользователям нужны эти типы для работы с вашим кодом.

Давайте разберёмся, почему. TypeScript типы (.d.ts файлы) — это исключительно информация для компилятора. Они нужны на этапе разработки и сборки, но в скомпилированном JavaScript коде их нет. Поэтому в продакшене, когда ваше приложение уже работает, типы не нужны.

Случай 1: Вы пишете конечное приложение (веб-сервер, CLI, desktop app)

В этом случае всегда кладите @types/* в devDependencies. Ваш package.json должен выглядеть так:

{
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "ts-node": "^10.9.2",
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.5",
    "@types/lodash": "^4.17.0",
    "@types/jest": "^29.5.11"
  }
}

Почему? Потому что при развёртывании вашего приложения на сервере (или при сборке Docker-образа) вы запускаете npm install --production (или npm ci --only=production). Эта команда устанавливает только dependencies, игнорируя devDependencies.

Если вы положите @types/express в dependencies, то в продакшене будут установлены десятки мегабайт ненужных файлов, которые замедлят развёртывание и увеличат размер образа. Более того, некоторые типы могут содержать ошибки или несовместимости, которые в продакшене вообще не должны проявляться.

Случай 2: Вы публикуете библиотеку (npm-пакет)

Здесь правило меняется. Если ваша библиотека использует типы из другого пакета, и эти типы "протекают" в публичный API вашей библиотеки, то их нужно указывать в dependencies.

Пример: вы пишете библиотеку my-express-utils, которая принимает Request и Response из Express.

// my-express-utils/index.ts
import { Request, Response } from 'express';

export function logRequest(req: Request): void {
  console.log(req.method, req.url);
}

export function sendJson(res: Response, data: object): void {
  res.json(data);
}

Пользователи вашей библиотеки будут импортировать logRequest и sendJson, и их TypeScript-компилятор должен знать типы Request и Response. Если вы положите @types/express в devDependencies, то при установке вашей библиотеки пользователь не получит эти типы, и его код не скомпилируется.

В этом случае @types/express должен быть в dependencies:

{
  "name": "my-express-utils",
  "dependencies": {
    "express": "^4.18.2",
    "@types/express": "^4.17.21"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.11.5"
  }
}

Практические рекомендации

  • Для приложений: все @types/* — в devDependencies. Это 99% случаев.
  • Для библиотек: @types/* — в dependencies, только если типы экспортируются или используются в публичном API. Иначе — в devDependencies.
  • Исключение: @types/node — обычно всегда в devDependencies, даже для библиотек, потому что пользователи сами устанавливают Node.js типы для своей среды.
  • Проверка: используйте npm ls @types/express чтобы увидеть, откуда приходит пакет. Если он появляется в дереве dependencies — значит, он установится в продакшене.

В вашем случае (приложение на Express) просто переместите все @types/* в devDependencies. Это уменьшит размер продакшен-сборки и ускорит развёртывание.

<<>>

Альтернативный подход: используйте peerDependencies для библиотек

Некоторые разработчики предпочитают не включать @types/* в dependencies даже для библиотек, а указывать их в peerDependencies. Это даёт пользователю гибкость в выборе версии типов.

{
  "name": "my-express-utils",
  "peerDependencies": {
    "@types/express": "^4.17.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.21"
  }
}

Плюс: пользователь сам решает, какую версию @types/express установить, и она не дублируется в его проекте.

Минус: если пользователь забудет установить @types/express, его код не скомпилируется, и он может не понять, в чём дело. Поэтому этот подход требует хорошей документации.

Для приложений этот метод не применяется — там по-прежнему используйте devDependencies.

Итог: для новичка самый безопасный путь — запомнить: "приложение → devDependencies, библиотека → dependencies (только для типов публичного API)". А когда наберётесь опыта, можно экспериментировать с peerDependencies.