Avatar ·

В чем разница между require(x) и import x в TypeScript/Node.js?

Всем привет! Я начал изучать TypeScript и Node.js, и меня смущает разница между двумя способами импорта модулей. Везде в старых туториалах я вижу такой код: ```typescript const express = require('express'); const fs = require('fs'); ``` Но в новых проектах на TypeScript пишут так: ```typescript import express from 'express'; import * as fs from 'fs'; ``` Я пытался разобраться сам, но запутался еще больше. Например, я попробовал сделать так: ```typescript // Старый способ const myModule = require('./myModule'); // Новый способ import myModule from './myModule'; ``` Оба вроде работают, но иногда require выдает ошибку "Cannot find module", а import работает. Или наоборот — import выдает "Module '".../myModule"' has no default export", а require работает нормально. Я использую Node.js 18 и TypeScript 5. В tsconfig.json у меня стоит "module": "commonjs". Может, дело в этом? Объясните, пожалуйста, какая между ними разница и когда что использовать. Особенно важно понять, почему import может не работать в одних случаях, а require — в других, и как правильно настраивать TypeScript для работы с ними. Заранее спасибо за помощь! <<>>

Основное различие: синтаксис и стандарты

require() и import — это два разных подхода к работе с модулями в JavaScript/TypeScript.

  • require() — это функция из CommonJS (CJS), старого стандарта модулей Node.js. Она синхронная, загружает модуль "на лету" и возвращает его содержимое.
  • import — это ключевое слово из ES-модулей (ESM), современного стандарта ECMAScript. Он асинхронный, статический (анализируется на этапе компиляции) и поддерживает tree-shaking.

Почему возникает путаница в TypeScript?

Проблема в том, что TypeScript — это надстройка над JavaScript. Он компилируется в JS, и то, во что превратится ваш import, зависит от настройки module в tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs" // или "esnext", "es2015", "node16" и т.д.
  }
}
  • Если "module": "commonjs" — TypeScript преобразует import в require(). Например:
// Исходный TS
import express from 'express';
// Станет в JS:
const express_1 = require("express");
const express = express_1.default;
  • Если "module": "esnext" — import останется как есть, и Node.js будет пытаться загрузить файл как ES-модуль. Для этого вам нужно либо добавить "type": "module" в package.json, либо использовать расширение .mjs.

Когда что использовать?

Используйте import (ESM), если:

  • Вы пишете новый проект на TypeScript.
  • Вы хотите использовать современные возможности: tree-shaking, статический анализ, асинхронную загрузку.
  • Ваш проект не требует совместимости со старыми CommonJS-пакетами.

Используйте require (CommonJS), если:

  • Вы работаете с легаси-кодом, который написан на CommonJS.
  • Вы используете старые версии Node.js (< 12).
  • Вам нужно динамически загружать модули (например, в условии). import() тоже может это делать, но синтаксис другой.

Практические примеры

Пример 1: Экспорт по умолчанию (default export)

// myModule.ts
export default function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Правильный импорт:
import greet from './myModule'; // ✅ Работает
const greet = require('./myModule').default; // ✅ Тоже работает, но нужно .default
const greet = require('./myModule'); // ❌ Вернет объект { default: function }

Пример 2: Именованный экспорт (named export)

// utils.ts
export const add = (a: number, b: number) => a + b;
export const multiply = (a: number, b: number) => a * b;

// Правильный импорт:
import { add, multiply } from './utils'; // ✅
const { add, multiply } = require('./utils'); // ✅ Тоже работает

Пример 3: Динамический импорт

// CommonJS (синхронный)
const moduleA = require('./moduleA');

// ESM (асинхронный)
const moduleA = await import('./moduleA');

Рекомендация для TypeScript

Установите в tsconfig.json:

{
  "compilerOptions": {
    "module": "node16", // или "nodenext" — это самый современный режим
    "moduleResolution": "node16",
    "esModuleInterop": true // важно для совместимости с CommonJS
  }
}

Опция esModuleInterop позволяет вам писать import express from 'express' даже для CommonJS-пакетов. Без нее вам пришлось бы писать import * as express from 'express'.

В итоге: для новых проектов используйте import. Это современный стандарт, который лучше поддерживается инструментами и дает больше возможностей. require() оставьте для старых проектов или для случаев, когда нужна динамическая загрузка (но и для этого есть import()).

<<>>

Дополнение: Как это работает под капотом

Стоит понимать, что require() — это просто функция, которую Node.js добавляет в каждый CommonJS-модуль. Она загружает код модуля, оборачивает его в функцию и выполняет. Все это происходит синхронно и в момент вызова.

import же работает иначе. Это не функция, а языковая конструкция. Модули анализируются до выполнения кода (на этапе парсинга). Поэтому import нельзя использовать внутри условий или функций:

// ❌ Так нельзя
if (condition) {
  import { something } from './module';
}

// ✅ А так можно
if (condition) {
  const module = await import('./module');
}

Для динамической загрузки существует import() (с круглыми скобками, как функция). Он возвращает Promise и работает и в ESM, и в CommonJS.

Еще один важный момент: кэширование. Обе системы кэшируют модули после первого require/import. Но в ESM кэш изолирован — если вы импортируете модуль через import и через require(), это могут быть два разных экземпляра модуля!

Поэтому не смешивайте два подхода в одном проекте без крайней необходимости. Выберите один (лучше ESM) и придерживайтесь его.