Экспорт и импорт
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 вопросов