ФАЗА 34. TYPESCRIPT Модули и npm

Обязательно

Код и зависимости собираются из явных частей

ESM · package.json · semver

Модули связывают экспорт и импорт, package.json описывает проект, lockfile делает установку воспроизводимой.

1. Модуль открывает только экспортированное

1 / 5

Локальные объявления остаются внутри файла, named и default export образуют разные способы публичного контракта.

  • Импорт указывает источник.
  • Named import сверяет имя.
  • Циклические зависимости усложняют порядок.
Локальный кодexportimport в другом модуле

2. ESM и CommonJS имеют разные механизмы

2 / 5

import/export работает статически, require и module.exports принадлежат CommonJS. Поле type влияет на интерпретацию .js в Node.

  • Формат выбирается на проект.
  • Расширения ESM указываются явно.
  • Interop не смешивается случайно.
ESM: import и exportCommonJS: require

3. package.json описывает проект и команды

3 / 5

Манифест хранит имя, тип модулей, скрипты и диапазоны зависимостей, а не сами установленные версии всех пакетов.

  • scripts стандартизируют запуск.
  • engines задаёт требования.
  • exports управляет публичными входами.
package.jsonСкрипты и зависимостиNode и инструменты

4. Runtime и dev зависимости различаются

4 / 5

dependencies нужны приложению в работе, devDependencies используются сборкой, тестами и анализом.

  • Компилятор обычно dev.
  • Runtime библиотека попадает в dependencies.
  • Production install уменьшает лишнее.
dependenciesdevDependencies

5. semver и lockfile ограничивают неожиданности

5 / 5

major, minor и patch описывают тип изменений, а lockfile фиксирует фактический граф для одинаковой установки в CI.

  • Диапазон не равен точной версии.
  • Ломающий major требует миграции.
  • Lockfile коммитится.
Диапазон semverLockfileВоспроизводимая установка

Модули и npm: как код собирается из частей

← назад к разделу

Реальный проект — это не один файл, а десятки и сотни. Чтобы код одного файла мог пользоваться функциями другого, существуют модули. А чтобы подключать чужой код (библиотеки) и не копировать его вручную, существует npm — пакетный менеджер из мира Node.js. Разберём оба по порядку.

Зачем нужны модули

Если бы весь код жил в одной глобальной области, любые две переменные с одинаковым именем конфликтовали бы, а понять, откуда взялась функция, было бы невозможно. Модуль решает это просто: каждый файл — отдельное пространство имён. Внутри файла всё своё и приватное; наружу видно только то, что вы явно экспортировали, а взять чужое можно только через явный импорт.

Короткая формула: один файл — один модуль, и связи между файлами видны в строках import.

ES-модули: import и export

Современный стандарт — ES modules (ESM). Это синтаксис import/export, единый для браузера и для Node. Именно его вы пишете в TypeScript по умолчанию.

// file: math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

// "export default" — одно главное значение на модуль
export default class Calculator {
  // ...
}
// file: app.ts
import Calculator, { add, PI } from "./math.js"; // default + именованные
import { add as plus } from "./math.js";          // переименование при импорте
import * as math from "./math.js";                // всё пространство модуля разом

console.log(add(2, 3)); // 5
console.log(math.PI);   // 3.14159

Что важно запомнить:

  • Именованный экспорт (export function add) импортируется в фигурных скобках по тому же имени.
  • Экспорт по умолчанию (export default) — один на файл, имя при импорте выбираете сами.
  • Путь к своему файлу начинается с ./ или ../. Путь без точки (import { z } from "zod") — это пакет из node_modules.
  • В ES-модулях под Node относительный путь пишут с расширением .js, даже если импортируете из .ts-файла: так будет называться скомпилированный файл, а Node подставлять расширения не умеет. С настройкой module: NodeNext компилятор на from "./math" ответит ошибкой TS2835 и сам подскажет ./math.js; tsx для разработки такое прощает, но собранный tsc код без расширения упадёт на Cannot find module.

CommonJS: require и module.exports

До появления ESM в Node была своя система модулей — CommonJS (CJS). Её вы будете встречать в унаследованном коде и многих библиотеках:

// экспорт
function add(a, b) {
  return a + b;
}
module.exports = { add };

// импорт
const { add } = require("./math");

Чем CommonJS отличается от ES-модулей на практике:

  • require() — обычный вызов функции, его можно поставить в середине файла или внутри условия. import — это объявление, которое поднимается наверх и выполняется до остального кода.
  • CommonJS грузит модули синхронно; ESM спроектирован под асинхронную загрузку и статический анализ (бандлеры умеют выкидывать неиспользуемый экспорт).
  • ESM — это будущее и стандарт; CommonJS — наследие, которое ещё долго будет рядом.

В Node то, какую систему использует ваш .js-файл, определяет поле "type" в package.json: "type": "module" — это ESM, "type": "commonjs" (или отсутствие поля) — CommonJS. В TypeScript вы почти всегда пишете import/export, а уже компилятор и настройки решают, во что это превратится.

package.json — паспорт проекта

Любой Node-проект описывается файлом package.json в корне. Это обычный JSON с метаданными проекта и, главное, списком зависимостей. Создать его можно командой npm init (а npm init -y — со значениями по умолчанию).

{
  "name": "my-service",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/app.js",
    "dev": "tsx watch src/app.ts",
    "test": "vitest"
  },
  "dependencies": {
    "express": "^4.19.2",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "typescript": "^5.4.5",
    "tsx": "^4.10.0",
    "vitest": "^1.6.0"
  }
}

dependencies против devDependencies

Зависимости делятся на две группы, и разница не косметическая:

  • dependencies — пакеты, без которых приложение не работает в продакшене. Например, веб-фреймворк express или валидатор zod. Они уезжают на сервер вместе с приложением.
  • devDependencies — инструменты, нужные только во время разработки и сборки: сам компилятор typescript, тестовый раннер, линтеры. На рабочем сервере собранного приложения они не нужны.

Команды кладут пакет в нужную группу: npm install express — в dependencies, npm install --save-dev typescript (короче npm i -D typescript) — в devDependencies. Правильное разделение уменьшает размер продакшен-сборки и снижает поверхность для уязвимостей.

semver — версии вида 4.19.2

Номер версии пакета — это semver (semantic versioning), три числа MAJOR.MINOR.PATCH:

  • MAJOR (4.x.x) — несовместимые изменения, старый код может сломаться.
  • MINOR (x.19.x) — новые возможности без поломок.
  • PATCH (x.x.2) — исправления, ничего не ломающие.

В package.json перед версией обычно стоит значок, который задаёт допустимый диапазон обновлений:

  • ^4.19.2 — «карет»: разрешено всё до следующего MAJOR (4.x.x, но не 5.0.0). Самый частый вариант.
  • ~4.19.2 — «тильда»: разрешены только PATCH-обновления (4.19.x).
  • 4.19.2 — строго эта версия, без обновлений.

Точные версии, которые реально установились, фиксируются в файле package-lock.json (у npm). Его коммитят в репозиторий — он гарантирует, что у вас и у коллеги соберётся ровно одно и то же.

node_modules и установка

Команда npm install (без аргументов) читает package.json, скачивает все зависимости и складывает их в папку node_modules в корне проекта. Туда же подтягиваются зависимости ваших зависимостей — поэтому папка получается огромной.

Два правила про node_modules:

  • Не коммитьте её в git — она восстанавливается из package.json + lock-файла одной командой. Добавьте node_modules/ в .gitignore.
  • Если что-то «сломалось непонятно почему», частое лечение — удалить node_modules и lock-файл и поставить заново.

npm, pnpm и другие

npm — менеджер по умолчанию, идёт вместе с Node. Есть альтернативы с тем же package.json: pnpm (хранит пакеты в одном общем месте и делает ссылки — экономит диск и ставит быстрее) и yarn. Для начала достаточно npm; pnpm стоит знать, потому что его часто выбирают в новых проектах.

Скрипты npm

Блок scripts в package.json — это именованные команды проекта. Запускаются они через npm run <имя>:

npm run build   # выполнит "tsc" — скомпилирует TypeScript
npm run dev     # запустит dev-режим с автоперезагрузкой
npm test        # для test/start/stop слово run можно опустить

Скрипты избавляют от запоминания длинных команд и документируют, как с проектом работать: открыв package.json, любой человек сразу видит, как его собрать, запустить и протестировать.

Дополнительно: при первом чтении можно пропустить

Глубже: обёртка модуля, exports против module.exports и циклические зависимостирасширенное

Три вопроса про CommonJS задают на собеседованиях по Node чаще остальных, и все три объясняются одним механизмом. Перед выполнением Node заворачивает содержимое файла в функцию: (function (exports, require, module, __filename, __dirname) { ... }). Поэтому у каждого файла свои require и module, переменные верхнего уровня не утекают в глобальную область, а __dirname всегда указывает на каталог именно этого файла.

Второй вопрос вытекает из первого. exports это всего лишь локальная ссылка на объект module.exports, которую обёртка передала параметром. Дописывать в него свойства можно, exports.add = add работает, а вот присвоить exports = { add } бесполезно: переменная теперь указывает на новый объект, а require вернёт прежний module.exports, пустой. Заменить экспорт целиком можно только через module.exports = ....

Третий: require кеширует модуль по абсолютному пути и выполняет файл один раз, повторные вызовы отдают тот же объект. Отсюда поведение циклических зависимостей: если a.js требует b.js, а тот в свою очередь требует a.js, то b получит недостроенный module.exports модуля a, тот, что успел заполниться до строки с require. Ошибки не будет, будет undefined в неожиданном месте. Лечат циклы выносом общей части в третий модуль или отложенным require внутри функции; в ES-модулях цикл тоже возможен, но там связывание живое, и импорт получит значение, когда оно появится.

Коротко

  • Модуль — это файл; наружу видно только export, взять чужое можно только через import.
  • ES modules (import/export) — современный стандарт; CommonJS (require) — наследие в Node-коде и старых библиотеках.
  • package.json — паспорт проекта: метаданные, зависимости и скрипты.
  • dependencies нужны в продакшене, devDependencies — только для разработки и сборки.
  • semver MAJOR.MINOR.PATCH; ^ разрешает обновления до следующего MAJOR, lock-файл фиксирует точные версии.
  • node_modules восстанавливается из package.json, в git не коммитится.
  • npm scripts запускаются через npm run <имя> и документируют работу с проектом.
  • CommonJS заворачивает файл в функцию с exports, require и module; заменять экспорт можно только через module.exports; require кеширует модуль, поэтому в цикле зависимостей второй модуль видит недостроенный экспорт первого.

Что почитать дальше