الوحدات والحزم والاستيراد في TypeScript: دليل شامل
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты строишь огромный дом. Ты можешь попытаться сделать всё сам: замесить бетон, вырезать окна, провести электричество, собрать мебель. Но это займёт годы, и результат, скорее всего, будет ужасным. Гораздо умнее поступить иначе: купить готовые блоки для стен, заказать пластиковые окна у специалистов, нанять электрика и купить мебель в магазине.
Точно так же устроено программирование. Когда ты пишешь программу на TypeScript, ты не обязан создавать всё с нуля. Ты можешь брать готовые «кирпичики» — модули и пакеты — и соединять их вместе. Это экономит время, нервы и делает код понятным.
Давай разберёмся с терминами на примере библиотеки:
- Модуль — это как одна книга на полке. В ней собраны знания по одной теме: например, «Как работать с датами» или «Как рисовать графики». В программировании модуль — это отдельный файл с кодом, который делает что-то конкретное.
- Пакет — это целая полка (или даже шкаф) с книгами. Пакет объединяет несколько модулей, которые нужны для решения большой задачи. Например, пакет «Строительство дома» может содержать модули: «Фундамент», «Стены», «Крыша».
- Импорт — это когда ты идёшь к полке, берёшь нужную книгу и начинаешь её читать. В коде это команда, которая говорит: «Возьми функцию или переменную из другого файла и дай мне её использовать».
💡 Важно: Модули и пакеты — это способ не изобретать велосипед. Если кто-то уже написал отличный код для работы с датами, зачем писать свой? Просто импортируй готовый!
Зачем это нужно? Представь, что твоя программа — это текст на 10 000 строк. Если всё хранить в одном файле, ты быстро запутаешься: где искать ошибку? Как понять, что делает каждая часть? Модули решают эту проблему: ты разбиваешь код на маленькие кусочки, каждый из которых отвечает за свою задачу. Это называется модульность.
В TypeScript (и в современном JavaScript) модули — это основа. Без них невозможно представить ни один серьёзный проект. Они помогают:
- Делить код на логические части.
- Переиспользовать код в разных проектах.
- Скрывать внутренние детали (не всё, что есть в модуле, обязательно показывать наружу).
- Работать в команде: каждый пишет свой модуль, а потом все соединяют.
В этой статье мы изучим, как работают модули и пакеты в TypeScript, как их импортировать и экспортировать, и как не наделать ошибок. Поехали!
Теория: как это работает изнутри
Чтобы понять модули, нужно заглянуть «под капот» TypeScript. Когда ты пишешь код, ты используешь систему модулей. Это набор правил, которые определяют, как файлы общаются друг с другом.
Что такое модуль в TypeScript?
Любой файл с расширением .ts или .tsx может стать модулем. Но есть условие: если в файле есть хотя бы одна команда import или export, он считается модулем. Если таких команд нет — это просто скрипт, и его переменные «видны» всем (что плохо, потому что можно случайно перезаписать чужие данные).
Модуль — это изолированное пространство. Всё, что ты объявляешь внутри модуля (переменные, функции, классы), по умолчанию видно только внутри этого модуля. Чтобы поделиться чем-то с другими файлами, нужно явно сказать: «Это я разрешаю использовать снаружи». Для этого используется команда export.
Как работает импорт?
Когда ты пишешь import { something } from './file', TypeScript (а потом и JavaScript) делает следующее:
- Находит файл
file.ts(илиfile.js, если это скомпилированный код). - Загружает его содержимое.
- Ищет в нём то, что помечено как
exportи совпадает по имени. - Делает это доступным в текущем файле.
Важно понимать: модули загружаются только один раз. Если два файла импортируют один и тот же модуль, он не будет загружен дважды. JavaScript умный: он кеширует модули. Это экономит память и время.
Пути и разрешение модулей
Когда ты указываешь путь в import, TypeScript ищет файл по определённым правилам. Например:
'./module'— относительный путь. Ищет файлmodule.tsв той же папке.'../utils/helper'— подняться на папку выше, потом зайти вutilsи найтиhelper.ts.'lodash'— без точки в начале. Это означает, что нужно искать пакет в папкеnode_modules. Так подключаются библиотеки, установленные через npm.
TypeScript также поддерживает разрешение модулей — он может искать файлы с разными расширениями: .ts, .tsx, .d.ts, .js. Это настраивается в файле tsconfig.json.
Компиляция: как TypeScript превращается в JavaScript
TypeScript — это надстройка над JavaScript. Браузеры и Node.js не понимают TypeScript напрямую. Поэтому код нужно скомпилировать (перевести) в JavaScript. При компиляции модули TypeScript превращаются в модули JavaScript. В зависимости от настроек, это могут быть:
- CommonJS (require/module.exports) — используется в Node.js.
- ES Modules (import/export) — современный стандарт для браузеров и новых версий Node.js.
- AMD, UMD, System — более старые форматы, сейчас редко используются.
В этой статье мы будем говорить о современном синтаксисе import/export, который поддерживается и TypeScript, и современным JavaScript.
⚠️ Важно: Если ты пишешь код для браузера, убедись, что твой сборщик (Webpack, Vite, Parcel) настроен на работу с ES модулями. Если для Node.js — проверь, что вpackage.jsonесть"type": "module".
Синтаксис в TypeScript: все формы и варианты
TypeScript предлагает несколько способов экспортировать и импортировать код. Давай разберём каждый из них с примерами.
Экспорт: как отдать код наружу
Есть два основных вида экспорта: именованный и экспорт по умолчанию.
1. Именованный экспорт (named export)
Ты можешь экспортировать несколько сущностей из одного модуля. Каждая из них имеет своё имя.
// файл: mathUtils.ts
// Экспортируем функцию
export function add(a: number, b: number): number {
return a + b;
}
// Экспортируем переменную
export const PI = 3.14159;
// Экспортируем класс
export class Calculator {
multiply(a: number, b: number): number {
return a * b;
}
}
2. Экспорт по умолчанию (default export)
Из модуля можно экспортировать одну главную сущность. Это удобно, когда модуль делает что-то одно.
// файл: logger.ts
// Экспорт по умолчанию — функция
export default function log(message: string): void {
console.log(`[LOG]: ${message}`);
}
Можно также экспортировать по умолчанию класс или объект:
// файл: config.ts
// Экспорт по умолчанию — объект
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
export default config;
3. Комбинированный экспорт
В одном модуле можно одновременно использовать именованный экспорт и экспорт по умолчанию. Но так делать не рекомендуется, чтобы не путать читателей кода.
// файл: utils.ts
export function helper1() { /* ... */ }
export function helper2() { /* ... */ }
export default function mainHelper() { /* ... */ }
4. Реэкспорт (re-export)
Иногда удобно собрать все экспорты из разных модулей в одном месте и экспортировать их снова. Это называется «баррель» (barrel).
// файл: index.ts
// Реэкспортируем всё из mathUtils
export * from './mathUtils';
// Реэкспортируем конкретные сущности
export { log } from './logger';
// Реэкспортируем по умолчанию под другим именем
export { default as appConfig } from './config';
Импорт: как взять код из другого модуля
Теперь посмотрим, как импортировать то, что мы экспортировали.
1. Импорт именованных экспортов
// файл: main.ts
// Импортируем конкретные функции и переменные
import { add, PI, Calculator } from './mathUtils';
// Теперь можно использовать
const sum = add(2, 3); // 5
const calc = new Calculator();
const result = calc.multiply(4, 5); // 20
console.log(PI); // 3.14159
2. Импорт с переименованием
Если имя экспортированной сущности конфликтует с чем-то в твоём файле, можно дать ей другое имя.
// Импортируем add, но называем её sumFunction
import { add as sumFunction } from './mathUtils';
console.log(sumFunction(10, 20)); // 30
3. Импорт всего модуля как объекта
Можно импортировать всё, что экспортирует модуль, в виде одного объекта.
// Импортируем всё из mathUtils в объект MathUtils
import * as MathUtils from './mathUtils';
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(1, 2)); // 3
const calc = new MathUtils.Calculator();
4. Импорт экспорта по умолчанию
Для импорта по умолчанию можно дать любое имя, потому что экспорт по умолчанию — это «главный» экспорт модуля.
// Импортируем функцию log из logger.ts
import log from './logger';
// Теперь можно использовать
log('Программа запущена');
5. Импорт и именованного, и по умолчанию
// Импортируем и default, и именованные экспорты
import log, { helper1, helper2 } from './utils';
log('Старт');
helper1();
helper2();
6. Импорт для побочных эффектов
Иногда модуль нужно просто выполнить, чтобы он что-то сделал (например, установил глобальные стили или полифиллы). В этом случае импортируют без фигурных скобок.
// Просто выполняем код из модуля
import './polyfills';
import './styles.css';
7. Импорт типов (TypeScript-specific)
TypeScript позволяет импортировать только типы, чтобы не включать их в скомпилированный JavaScript. Это оптимизация.
// Импортируем только тип
import type { User } from './models';
// Или импортируем и тип, и значение
import { type User, createUser } from './models';
Справочная таблица: формы экспорта и импорта
| Действие | Синтаксис | Пример |
|---|---|---|
| Именованный экспорт | export function/const/class |
export const PI = 3.14; |
| Экспорт по умолчанию | export default |
export default function() {} |
| Импорт именованных | import { name } from 'path' |
import { add } from './math' |
| Импорт с переименованием | import { name as alias } |
import { add as sum } |
| Импорт всего модуля | import * as obj from 'path' |
import * as Utils from './utils' |
| Импорт по умолчанию | import name from 'path' |
import log from './logger' |
| Импорт типов | import type { Type } from 'path' |
import type { User } from './models' |
| Реэкспорт | export * from 'path' |
export * from './math' |
Примеры кода — от простого к сложному
Давай посмотрим на реальные примеры, как модули работают в проекте. Мы создадим маленькое приложение для управления списком задач (todo list).
Пример 1: Простой модуль с одной функцией
// Файл: greetings.ts
// Простой модуль, который экспортирует одну функцию
export function sayHello(name: string): string {
return `Привет, ${name}!`;
}
// Файл: app.ts
// Импортируем функцию и используем её
import { sayHello } from './greetings';
const message = sayHello('Анна');
console.log(message); // Выведет: Привет, Анна!
Объяснение: В первом файле мы создали функцию и пометили её словом export. Во втором файле мы импортировали её с помощью фигурных скобок и использовали. Всё просто!
Пример 2: Модуль с несколькими экспортами
// Файл: todoTypes.ts
// Определяем типы и интерфейсы для нашего приложения
export interface Task {
id: number;
title: string;
completed: boolean;
}
export type TaskStatus = 'active' | 'completed' | 'all';
export const DEFAULT_STATUS: TaskStatus = 'all';
// Файл: todoService.ts
// Сервис для работы с задачами
import { Task, TaskStatus, DEFAULT_STATUS } from './todoTypes';
// Хранилище задач (в реальном проекте здесь была бы база данных)
let tasks: Task[] = [];
let nextId = 1;
export function addTask(title: string): Task {
const newTask: Task = {
id: nextId++,
title,
completed: false
};
tasks.push(newTask);
return newTask;
}
export function getTasks(status: TaskStatus = DEFAULT_STATUS): Task[] {
if (status === 'all') {
return tasks;
}
return tasks.filter(task =>
status === 'completed' ? task.completed : !task.completed
);
}
export function toggleTask(id: number): boolean {
const task = tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
return true;
}
return false;
}
Объяснение: Здесь мы разделили типы и логику. todoTypes.ts содержит только описания данных, а todoService.ts — функции для работы с ними. Обрати внимание, что переменная tasks не экспортируется — она скрыта внутри модуля. Это называется инкапсуляция.
Пример 3: Экспорт по умолчанию для главного класса
// Файл: taskRenderer.ts
// Класс для отрисовки задач в HTML (если бы мы работали с браузером)
export default class TaskRenderer {
private container: HTMLElement;
constructor(containerId: string) {
const el = document.getElementById(containerId);
if (!el) {
throw new Error(`Элемент с id "${containerId}" не найден`);
}
this.container = el;
}
render(tasks: Task[]): void {
this.container.innerHTML = tasks.map(task => `
${task.title}
`).join('');
}
}
// Файл: main.ts
// Главный файл приложения
import TaskRenderer from './taskRenderer';
import { addTask, getTasks } from './todoService';
// Создаём рендерер
const renderer = new TaskRenderer('app');
// Добавляем несколько задач
addTask('Изучить модули TypeScript');
addTask('Написать статью');
addTask('Сделать домашнее задание');
// Получаем все задачи и отображаем
const allTasks = getTasks();
renderer.render(allTasks);
Объяснение: Класс TaskRenderer — это главная сущность модуля, поэтому мы экспортируем его по умолчанию. В main.ts мы импортируем его без фигурных скобок и даём любое имя (в данном случае TaskRenderer).
Пример 4: Использование пакетов из npm
// Файл: dateHelper.ts
// Используем популярный пакет date-fns для работы с датами
// Сначала нужно установить пакет: npm install date-fns
import { format, addDays, isWeekend } from 'date-fns';
export function formatDate(date: Date): string {
// Форматируем дату в русском формате
return format(date, 'dd.MM.yyyy');
}
export function getNextWorkday(date: Date): Date {
let nextDay = addDays(date, 1);
// Пропускаем выходные
while (isWeekend(nextDay)) {
nextDay = addDays(nextDay, 1);
}
return nextDay;
}
Объяснение: Пакет date-fns — это библиотека, установленная через npm. Мы импортируем только те функции, которые нам нужны (format, addDays, isWeekend). Это называется деревом тряски (tree shaking) — в итоговый код попадут только используемые функции, а не вся библиотека.
Пример 5: Реэкспорт через индексный файл
// Файл: models/index.ts
// Собираем все модели в одном месте
export { Task } from './task';
export { User } from './user';
export { Project } from './project';
export type { TaskStatus } from './task';
// Файл: app.ts
// Теперь можно импортировать всё из одной папки
import { Task, User, Project } from './models';
const task: Task = { id: 1, title: 'Купить молоко', completed: false };
const user: User = { id: 1, name: 'Иван' };
Объяснение: Вместо того чтобы запоминать, в каком файле какой тип лежит, мы создаём «баррель» — файл index.ts, который реэкспортирует всё из папки. Теперь можно импортировать всё одной строкой.
Пример 6: Динамический импорт
// Файл: heavyModule.ts
// Предположим, это тяжёлый модуль, который не нужно загружать сразу
export function calculateComplexity(data: number[]): number {
// Какие-то сложные вычисления
return data.reduce((acc, val) => acc + Math.sqrt(val), 0);
}
// Файл: app.ts
// Загружаем модуль только когда он понадобится
async function processData(data: number[]) {
// Динамический импорт — модуль загрузится только здесь
const heavyModule = await import('./heavyModule');
const result = heavyModule.calculateComplexity(data);
console.log(`Результат: ${result}`);
}
// Модуль не загружен, пока не вызвана функция
console.log('Приложение запущено');
// Вызываем функцию — модуль загружается
processData([1, 2, 3, 4, 5]);
Объяснение: Динамический импорт (import()) возвращает Promise. Это позволяет загружать модули «лениво» — только когда они действительно нужны. Это ускоряет начальную загрузку приложения.
Пример 7: Импорт типов и интерфейсов
// Файл: apiTypes.ts
// Определяем типы для API
export interface ApiResponse {
data: T;
status: number;
message: string;
}
export type UserRole = 'admin' | 'user' | 'guest';
export interface UserProfile {
id: number;
name: string;
role: UserRole;
}
// Файл: apiClient.ts
// Клиент для работы с API
// Импортируем только типы — они не попадут в скомпилированный код
import type { ApiResponse, UserProfile } from './apiTypes';
export async function fetchUser(id: number): Promise> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Объяснение: Ключевое слово type в импорте говорит TypeScript: «Мне нужны только типы, не включай их в JavaScript». Это делает итоговый код легче и чище.
Сравнение с Python: чем похоже и чем отличается
Если ты уже знаком с Python, тебе будет проще понять модули в TypeScript. Давай сравним их.
| Характеристика | TypeScript | Python |
|---|---|---|
| Импорт модуля | import { name } from './module' |
from module import name |
| Импорт всего модуля | import * as mod from './module' |
import module |
| Экспорт по умолчанию | export default function() {} |
Нет прямого аналога (в Python всё — именованный экспорт) |
| Реэкспорт | export * from './module' |
Нет прямого аналога (можно через __init__.py) |
| Пакеты | Папка с index.ts или package.json |
Папка с __init__.py |
| Установка пакетов | npm install package |
pip install package |
| Динамический импорт | import('./module') (асинхронно) |
importlib.import_module('module') |
| Импорт типов | import type { Type } |
Нет аналога (типы — в документации) |
Основные отличия:
- Экспорт по умолчанию: В Python нет понятия «главного» экспорта. В TypeScript это удобно, когда модуль делает что-то одно.
- Фигурные скобки: В TypeScript при импорте именованных экспортов нужны фигурные скобки. В Python — нет.
- Пути: В TypeScript нужно указывать расширение файла (хотя современные сборщики могут его опускать). В Python расширение
.pyне указывается. - Типы: TypeScript позволяет импортировать только типы, что оптимизирует код. В Python типы — это просто аннотации, они не влияют на импорт.
- Асинхронность: Динамический импорт в TypeScript асинхронный (возвращает Promise). В Python — синхронный.
💡 Совет: Если ты переходишь с Python на TypeScript, запомни главное правило: в TypeScript ты всегда явно указываешь, что экспортируешь. В Python всё, что не начинается с подчёркивания, считается публичным. В TypeScript — наоборот: всё приватное, пока не скажешь export.
Типичные ошибки и подводные камни
Даже опытные разработчики иногда попадают в ловушки при работе с модулями. Вот самые распространённые ошибки.
Ошибка 1: Забыл указать расширение файла
// ❌ Неправильно
import { add } from './mathUtils';
// ✅ Правильно (в большинстве случаев)
import { add } from './mathUtils.ts';
// Или, если настроен сборщик, можно без расширения
Почему это проблема: В старых версиях Node.js и некоторых сборщиках нужно указывать расширение. Лучше всегда указывать .ts или .js (в зависимости от того, что ты компилируешь).
Ошибка 2: Циклические зависимости
// Файл: a.ts
import { b } from './b';
export const a = b + 1;
// Файл: b.ts
import { a } from './a';
export const b = a + 1;
Почему это проблема: Модули пытаются загрузить друг друга, и возникает бесконечный цикл. TypeScript может выдать ошибку или вернуть undefined. Решение: пересмотреть архитектуру и вынести общие зависимости в третий модуль.
Ошибка 3: Путаница между default и named экспортом
// ❌ Неправильно: пытаемся импортировать default как именованный
import { log } from './logger'; // Если log — это export default
// ✅ Правильно:
import log from './logger';
Почему это проблема: Если ты используешь фигурные скобки, TypeScript ищет именованный экспорт. Если его нет — ошибка. Запомни: default экспорт импортируется без фигурных скобок.
Ошибка 4: Импорт несуществующего экспорта
// ❌ Ошибка: функция multiply не экспортируется
import { multiply } from './mathUtils';
// ✅ Нужно проверить, что экспортируется
import { add, PI, Calculator } from './mathUtils';
Почему это проблема: TypeScript выдаст ошибку на этапе компиляции. Но если ты используешь JavaScript без строгой типизации, ошибка проявится только в рантайме.
Ошибка 5: Импорт типов как значений
// ❌ Неправильно: пытаемся использовать тип как значение
import { User } from './models';
const user = new User(); // Ошибка! User — это интерфейс, не класс
// ✅ Правильно: интерфейсы — это только для описания формы объекта
import type { User } from './models';
const user: User = { id: 1, name: 'Анна' };
Поч