$ sudo teach IT
МОДУЛЬ 9 · УРОК 1

Экспорт и импорт

export, import, named exports, импорт всего модуля

📖 Введение

Представьте, что вы живёте в большом доме с несколькими комнатами. В каждой комнате есть свои вещи: в кухне — кастрюли и тарелки, в спальне — кровать и шкаф, в гостиной — диван и телевизор. Если вам нужно взять что-то из кухни, вы идёте на кухню и берёте это. Но что если бы каждая вещь была подписана, и вы точно знали, где что лежит? Было бы удобнее, правда?

Точно так же устроены модули в TypeScript. Каждый модуль — это как комната в доме. В нём живут свои функции, переменные и классы. Чтобы использовать что-то из другого модуля, вам нужно «пойти в эту комнату» — импортировать нужное. А чтобы другие могли использовать то, что есть в вашем модуле, нужно это «показать» — экспортировать.

В этом уроке мы подробно разберём, как работают export и import — два ключевых механизма, которые позволяют разбивать код на модули и использовать его повторно. Без модулей TypeScript-проект превратился бы в один огромный файл на тысячи строк — разобраться в нём было бы невозможно.

1 Зачем нужны модули — от хаоса к порядку

Без модулей код — это один длинный файл. Допустим, вы пишете веб-приложение. У вас есть функции для работы с пользователями, функции для расчёта цен, функции для отправки email-писем. Всё свалено в один файл app.ts:

// app.ts — ВСЁ в одном файле
function createUser(name: string, email: string) { /* ... */ }
function deleteUser(id: number) { /* ... */ }
function calculatePrice(base: number, tax: number) { /* ... */ }
function applyDiscount(price: number, discount: number) { /* ... */ }
function sendEmail(to: string, subject: string, body: string) { /* ... */ }
function sendBulkEmail(recipients: string[], subject: string) { /* ... */ }

Проблемы такого подхода очевидны:

  • Файл растёт бесконтрольно — через месяц он будет на 5000 строк
  • Найти нужную функцию сложно — приходится скроллить
  • Нельзя переиспользовать код — если другой проект хочет ту же функцию, копируем
  • Конфликты имен — что если в другом месте уже есть функция sendEmail?
  • Тестирование сложно — как протестировать только работу с пользователями?

Модули решают все эти проблемы. Вот как выглядит тот же проект, разбитый на модули:

// users.ts
export function createUser(name: string, email: string) { /* ... */ }
export function deleteUser(id: number) { /* ... */ }

// pricing.ts
export function calculatePrice(base: number, tax: number) { /* ... */ }
export function applyDiscount(price: number, discount: number) { /* ... */ }

// email.ts
export function sendEmail(to: string, subject: string, body: string) { /* ... */ }
export function sendBulkEmail(recipients: string[], subject: string) { /* ... */ }

Теперь код разделён по логическим группам. Каждый модуль отвечает за свою область. Это как шкафы с подписанными ящиками — знаете, где что лежит.

2 export — экспорт именованных переменных

Ключевое слово export делает переменную, функцию или класс доступными для других модулей. Без export всё внутри модуля остаётся приватным — его нельзя импортировать извне.

Аналогия: Экспорт — это как вывеска магазина. Без вывески люди незнает, что за дверью находится магазин. С вывеской — знают и могут зайти. Без export модуль существует, но его содержимое невидимо для мира снаружи.

Базовый синтаксис — просто добавьте export перед объявлением:

// math.ts
export const PI = 3.14159;

export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export class Calculator {
  private history: number[] = [];

  calculate(a: number, op: string, b: number): number {
    let result: number;
    switch (op) {
      case '+': result = a + b; break;
      case '-': result = a - b; break;
      case '*': result = a * b; break;
      case '/': result = a / b; break;
      default: throw new Error('Unknown operator');
    }
    this.history.push(result);
    return result;
  }

  getHistory(): number[] {
    return [...this.history];
  }
}

В этом модуле экспортированы:

  • Константа PI
  • Функции add и subtract
  • Класс Calculator

Вы также можете экспортировать уже объявленные переменные, добавив export в конце файла:

// utils.ts
const MAX_RETRIES = 3;
const TIMEOUT = 5000;

export { MAX_RETRIES, TIMEOUT };

// Или с переименованием:
export { MAX_RETRIES as maxRetries, TIMEOUT as timeout };

Совет: Именованные экспорты позволяют импортировать только то, что нужно. Это помогает с tree-shaking — процессом удаления неиспользуемого кода при сборке. Если вы импортируете только add из модуля math.ts, бандлер может не включать subtract и Calculator в итоговый бандл.

3 import — импорт именованных экспортов

Когда что-то экспортировано из модуля, его можно импортировать в другой файл с помощью ключевого слова import. Вот базовый синтаксис:

import { имя1, имя2 } from './путь/к/модулю';

Давайте импортируем то, что мы экспортировали в предыдущем примере:

// app.ts
import { add, subtract, PI, Calculator } from './math';

console.log(add(2, 3));         // 5
console.log(subtract(10, 4));   // 6
console.log(PI);                // 3.14159

const calc = new Calculator();
console.log(calc.calculate(5, '+', 3));  // 8
console.log(calc.getHistory());          // [8]

Импорты записываются в фигурных скобках { } — это называется именованный импорт (named import). Вы перечисляете конкретные имена, которые хотите получить из модуля.

Переименование при импорте (alias):

// Если имя конфликтует с локальной переменной
import { add as sum, subtract as diff } from './math';

console.log(sum(2, 3));  // Вместо add используем sum
console.log(diff(10, 4));

Переименование полезно в нескольких ситуациях:

  • Когда имя конфликтует с локальной переменной
  • Когда оригинальное имя слишком длинное или неудобное
  • Когда вы хотите дать имя, более подходящее к контексту

Важно: Имена при импорте должны точно совпадать с именами при экспорте. TypeScript чувствителен к регистру — Add и add — это разные имена.

4 import * as — импорт всего модуля

Иногда нужно импортировать все экспорты из модуля сразу. Для этого используется import * as:

import * as math from './math';

console.log(math.add(2, 3));         // 5
console.log(math.subtract(10, 4));   // 6
console.log(math.PI);                // 3.14159

const calc = new math.Calculator();
console.log(calc.calculate(5, '*', 3));  // 15

Здесь math — это объект, в котором собраны все экспорты из модуля. Вы обращаетесь к ним через точку: math.add, math.PI и т.д.

Плюсы:

  • Сразу видно, откуда берётся каждая функция — math.add сразу понятно
  • Не нужно перечислять каждое имя — берёте всё
  • Нет конфликтов имён — всё лежит под одним пространством имён

Минусы:

  • Код становится более многословным — math.add вместо просто add
  • Не помогает с tree-shaking — бандлер не знает, какие части модуля вы используете
  • Может привести к импорту лишнего кода

Когда использовать: Когда вы часто используете много функций из одного модуля и хотите явно показать их происхождение. Например, при работе с утилитарными библиотеками типа Lodash:

import * as _ from 'lodash';

const arr = [1, 2, 3, 4, 5];
const doubled = _.map(arr, (n) => n * 2);
const filtered = _.filter(arr, (n) => n > 2);

5 Переименование: export as и import as

Иногда вам нужно экспортировать функцию под другим именем — например, когда оригинальное имя конфликтует в целевом модуле:

// utils.ts
function process(data: string): string {
  return data.toUpperCase();
}

// Экспортируем под другим именем
export { process as processData };

Теперь в другом модуле вы импортируете под новым именем:

import { processData } from './utils';
console.log(processData('hello'));  // HELLO

Комбинирование: Вы можете комбинировать импорт и переименование:

// Импортируем с переименованием
import { processData as process } from './utils';
console.log(process('hello'));  // HELLO

// Или импортируем всё и используем через объект
import * as utils from './utils';
console.log.utils.processData('hello');  // HELLO

6 Путь импорта: относительные и абсолютные пути

В строке from './путь' указывается, откуда импортировать. Пути бывают двух видов:

1. Относительные пути — начинаются с ./ или ../:

// Из текущей папки
import { add } from './math';

// Из родительской папки
import { add } from '../utils/math';

// Из вложенной папки
import { add } from './utils/math';

2. Абсолютные пути (алиасы) — настраиваются в tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["src/utils/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

Теперь можно импортировать через алиасы:

import { add } from '@utils/math';
import { Button } from '@components/Button';

Абсолютные пути делают код чище — не нужно считать количество ../. Но они требуют настройки в tsconfig.json и в бандлере (Webpack, Vite и т.д.).

7 Опциональный импорт и side-effect импорты

Side-effect импорт: Иногда модуль ничего не экспортирует, но делает что-то при загрузке — например, добавляет стили или регистрирует плагин:

// Импортируем просто дляпобочный эффект (side-effect)
import './styles.css';
import './polyfills';

Здесь не импортируются никакие имена — мы просто хотим, чтобы код в styles.css или polyfills.ts выполнился.

Опциональный импорт: В TypeScript можно импортировать имена опционально, используя import type:

// Импортируем только тип — не создаёт JavaScript-код
import type { User, Order } from './types';

// Импортируем и значение, и тип
import { createUser } from './users';
import type { User } from './types';

import type используется, когда вам нужен только тип для аннотаций. TypeScript удалит этот импорт при компиляции — он не попадёт в итоговый JavaScript.

⚠️ Типичные ошибки при работе с модулями

Вот самые распространённые ошибки, которые допускают новички:

1. Забыли расширение файла:

// ❌ Неправильно — забыли .ts
import { add } from './math';

// ✅ Правильно (в большинстве случаев)
import { add } from './math.ts';

В TypeScript расширение .ts указывать не нужно — компилятор находит файл автоматически. Но если используете moduleResolution не node, может понадобиться расширение.

2. Неправильный путь:

// ❌ Неправильно — неполный путь
import { add } from 'math';

// ✅ Правильно — указываем относительный путь
import { add } from './math';

Без ./ TypeScript ищет модуль в папке node_modules, а не в вашем проекте.

3. Дублирование имён:

// ❌ Неправильно — два модуля экспортируют 'add'
import { add } from './math';
import { add } from './utils';  // Ошибка: 'add' уже импортирован

// ✅ Правильно — используем переименование
import { add } from './math';
import { add as addUtil } from './utils';

4. Импорт несуществующего имени:

// ❌ Неправильно — 'multiply' не экспортирован из math.ts
import { add, multiply } from './math';  // Ошибка!

TypeScript сразу покажет ошибку, если вы пытаетесь импортировать то, что не экспортировано. Это одно из главных преимуществ TypeScript — ошибки видны до запуска программы.

🛠 Практический пример: структура проекта

Давайте посмотрим, как модули используются в реальном проекте. Представьте интернет-магазин:

src/
├── models/
│   ├── User.ts
│   ├── Product.ts
│   └── Order.ts
├── services/
│   ├── UserService.ts
│   ├── ProductService.ts
│   └── OrderService.ts
├── utils/
│   ├── math.ts
│   ├── string.ts
│   └── date.ts
└── app.ts

Вот как выглядит модуль User.ts:

// src/models/User.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  createdAt: Date;
}

export function createUser(name: string, email: string): User {
  return {
    id: Math.floor(Math.random() * 10000),
    name,
    email,
    role: 'user',
    createdAt: new Date()
  };
}

export function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

А вот UserService.ts импортирует и использует:

// src/services/UserService.ts
import { User, createUser, isValidEmail } from '../models/User';
import { formatDate } from '../utils/date';

export class UserService {
  private users: User[] = [];

  addUser(name: string, email: string): User | null {
    if (!isValidEmail(email)) {
      console.log('Некорректный email');
      return null;
    }

    const user = createUser(name, email);
    this.users.push(user);
    return user;
  }

  getUserInfo(user: User): string {
    return `${user.name} (${user.email}) — зарегистрирован ${formatDate(user.createdAt)}`;
  }

  findByRole(role: User['role']): User[] {
    return this.users.filter(u => u.role === role);
  }
}

Обратите внимание на структуру: UserService импортирует только то, что нужно — интерфейс User, функции createUser и isValidEmail, а также утилиту formatDate. Каждый модуль отвечает за свою задачу.

✅ Итого

  • export — делает переменные, функции и классы доступными для других модулей
  • import — позволяет использовать экспорты из других модулей
  • Именованные экспорты — export { name } и import { name } — работают с конкретными именами
  • import * as — импортирует весь модуль как объект
  • export as / import as — позволяет переименовывать при экспорте/импорте
  • import type — импортирует только типы (удаляется при компиляции)
  • Пути импорта: относительные (./, ../) и абсолютные (алиасы через paths в tsconfig.json)

8 Динамический импорт — import()

Помимо статического импорта, JavaScript (и TypeScript) поддерживают динамический импорт — загрузку модуля «на лету», во время выполнения программы. Это полезно для:

  • Ленивой загрузки — загружаем модуль только когда он нужен
  • Условной загрузки — загружаем модуль в зависимости от условия
  • Разбиения кода — бандлер разбивает код на chunks
// Статический импорт — загружается сразу при старте
import { heavyFunction } from './heavy-module';

// Динамический импорт — загружается по требованию
async function loadHeavyModule() {
  const { heavyFunction } = await import('./heavy-module');
  return heavyFunction();
}

// Условная загрузка
async function processData(useCache: boolean) {
  if (useCache) {
    const { CacheService } = await import('./cache-service');
    return new CacheService();
  } else {
    const { DataService } = await import('./data-service');
    return new DataService();
  }
}

Важно: Динамический импорт возвращает Promise. Вы должны использовать await или .then(), чтобы получить доступ к экспортируемым именам.

TypeScript автоматически определяет типы динамического импорта:

// TypeScript автоматически определяет типы
async function load() {
  const utils = await import('./utils');
  // utils — это объект с типами из utils.ts
  console.log(utils.add(1, 2));  // ✅ TypeScript знает типы
}

9 Tree-shaking и модули — оптимизация размера бандла

Tree-shaking — это процесс удаления неиспользуемого кода при сборке проекта. Бандлер (Webpack, Vite, esbuild) анализирует, какие экспорты вы фактически используете, и удаляет остальные.

Пример: Допустим, вы импортируете только add из модуля с 100 функциями:

// utils.ts — 100 функций
export function add(a: number, b: number): number { return a + b; }
export function subtract(a: number, b: number): number { return a - b; }
export function multiply(a: number, b: number): number { return a * b; }
// ... ещё 97 функций

// app.ts — импортируем только add
import { add } from './utils';
console.log(add(1, 2));

С tree-shaking бандлер включит в итоговый файл только функцию add, а остальные 99 функций будут удалены. Это значительно уменьшает размер бандла.

Именованные экспорты работают лучше с tree-shaking, потому что бандлер точно знает, что именно импортируется. Default экспорты и import * менее эффективны для tree-shaking.

Проверяем понимание

5 вопросов

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

Premium

Импорт с переименованием

Premium