Перейти к содержимому

62. Namespaces и модули

Иллюстрация к уроку

Эффективная организация кода критически важна для поддерживаемости и масштабируемости любого проекта. TypeScript предоставляет два основных механизма для инкапсуляции и структурирования кода: Namespaces (пространства имен) и Modules (модули). Понимание их различий, преимуществ и сценариев использования необходимо для написания чистого, типобезопасного и современного TypeScript кода.

Проблема: Загрязнение глобальной области видимости

Заголовок раздела «Проблема: Загрязнение глобальной области видимости»

Без механизмов инкапсуляции, весь код находится в глобальной области видимости, что приводит к конфликтам имен и затрудняет понимание зависимостей.

logger.ts
function logMessage(message: string) {
console.log(`[LOG]: ${message}`);
}
// authenticator.ts
// Ой, функция с тем же именем! Это перезапишет предыдущую.
function logMessage(user: string) {
console.log(`[AUTH]: User ${user} authenticated.`);
}
logMessage("Приложение запущено"); // Выведет "[AUTH]: User Приложение запущено authenticated."
// Мы потеряли оригинальную функцию logMessage

Это классический пример проблемы, которую решают Namespaces и Modules, предоставляя изолированные области видимости.

Namespaces (ранее “Internal Modules”) — это способ логической группировки кода внутри глобальной области видимости или другой области видимости. Они полезны для организации кода в библиотеках, когда вы хотите предоставить структурированный API без использования системы модулей.

validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// app.ts
/// <reference path="validation.ts" /> // Для компиляции старых проектов или с `--outFile`
let strings = ["Hello", "98052", "101"];
// Используем валидаторы из пространства имен Validation
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
strings.forEach(s => {
for (let name in validators) {
let validator = validators[name];
let isMatch = validator.isAcceptable(s);
console.log(`'${s}' ${isMatch ? "matches" : "does not match"} ${name}`);
}
});
/*
'Hello' matches Letters only
'Hello' does not match ZIP code
'98052' does not match Letters only
'98052' matches ZIP code
'101' does not match Letters only
'101' does not match ZIP code
*/

В этом примере Validation предоставляет изолированную среду для своих интерфейсов и классов. Обратите внимание на ключевое слово export внутри namespace для того, чтобы члены были доступны извне.

Модули являются предпочтительным способом организации кода в современном TypeScript. Они основаны на стандарте ES Modules (ESM) и предоставляют мощный, эксплицитный механизм для управления зависимостями. Каждый файл, содержащий import или export на верхнем уровне, считается модулем.

// math.ts - Модуль для математических операций
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export const PI = 3.14159;
// calculator.ts - Модуль для более сложных вычислений
import { add, PI } from './math'; // Импорт именованных экспортов
export class Calculator {
circleArea(radius: number): number {
return PI * add(radius, radius); // Используем add из math.ts
}
}
// app.ts - Главное приложение
import { add, subtract } from './math';
import { Calculator } from './calculator';
// import * as MathOperations from './math'; // Импорт всего модуля как объекта
console.log(`2 + 3 = ${add(2, 3)}`); // 5
console.log(`5 - 2 = ${subtract(5, 2)}`); // 3
const calc = new Calculator();
console.log(`Area of circle with radius 5: ${calc.circleArea(5)}`); // 31.4159

Модули обладают рядом преимуществ:

  • Явные зависимости: Легко отслеживать, откуда приходят данные.
  • Изоляция: Переменные и функции, не экспортированные из модуля, приватны.
  • Оптимизация: Инструменты сборки (Webpack, Rollup, esbuild) могут использовать Tree Shaking для удаления неиспользуемого кода.
  • Стандартизация: Соответствуют веб-стандартам ES Modules.

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

Часто полезно собрать все экспорты из нескольких модулей в один “фасадный” модуль для удобства импорта.

src/utils/string-utils.ts
export function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); }
// src/utils/number-utils.ts
export function isEven(n: number): boolean { return n % 2 === 0; }
// src/utils/index.ts - Фасадный модуль
export * from './string-utils'; // Переэкспорт всего из string-utils
export { isEven } from './number-utils'; // Переэкспорт только isEven из number-utils
export { add } from '../math'; // Переэкспорт add из более высокого уровня
// app.ts
import { capitalize, isEven, add } from './src/utils';
console.log(capitalize("hello")); // Hello
console.log(isEven(4)); // true
console.log(add(10, 5)); // 15

Импорт и Экспорт только типов (import type, export type)

Заголовок раздела «Импорт и Экспорт только типов (import type, export type)»

Начиная с TypeScript 3.8, можно явно указывать, что импорт или экспорт относится только к типам, что полезно для сборщиков и для ясности кода. Эти импорты удаляются на этапе компиляции, не оставляя следа в рантайме.

// user.d.ts (или user.ts)
export interface User {
id: string;
name: string;
}
// auth.ts
import type { User } from './user'; // Импортируем только тип User
export function authenticate(user: User): boolean {
// Логика аутентификации...
console.log(`Authenticating user: ${user.name}`);
return user.id === "admin";
}
// app.ts
import { authenticate } from './auth';
// import { User } from './user'; // Здесь мы импортируем User для использования как значения, если это нужно
const adminUser = { id: "admin", name: "Administrator" };
console.log(authenticate(adminUser)); // true

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

async function loadAndUseUtils() {
if (Math.random() > 0.5) {
const { capitalize } = await import('./src/utils/string-utils');
console.log(capitalize("dynamic load")); // Dynamic load
} else {
console.log("String utils not loaded.");
}
}
loadAndUseUtils();

Объединение объявлений с Namespaces (Declaration Merging)

Заголовок раздела «Объединение объявлений с Namespaces (Declaration Merging)»

Namespaces могут быть объявлены несколько раз в разных файлах (или даже в одном), и TypeScript автоматически объединит их в одно пространство имен. Это позволяет расширять существующие пространства имен.

shapes.ts
namespace Geometry {
export interface Point { x: number; y: number; }
export function distance(p1: Point, p2: Point): number {
const dx = p1.x - p2.x;
const dy = p1.y - p2.y;
return Math.sqrt(dx * dx + dy * dy);
}
}
// more-shapes.ts
namespace Geometry { // Объединяем с существующим пространством имен Geometry
export class Circle {
constructor(public center: Point, public radius: number) {}
area(): number {
return Math.PI * this.radius * this.radius;
}
}
// Здесь Point будет доступен из первой декларации Geometry
}
// app.ts
/// <reference path="shapes.ts" />
/// <reference path="more-shapes.ts" />
const p1: Geometry.Point = { x: 0, y: 0 };
const p2: Geometry.Point = { x: 3, y: 4 };
console.log(`Distance: ${Geometry.distance(p1, p2)}`); // Distance: 5
const c = new Geometry.Circle(p1, 10);
console.log(`Circle Area: ${c.area()}`); // Circle Area: 314.159...

В этом примере Geometry содержит и функцию distance, и класс Circle, хотя они были объявлены в разных файлах.

Namespaces предоставляют способ логической группировки кода и были особенно актуальны до повсеместного распространения ES Modules. Они по-прежнему полезны в специфических сценариях, например, при разработке глобальных библиотек, где вы хотите избежать экспорта каждого члена в виде отдельного модуля.

Modules (ES Modules) — это современный, рекомендуемый подход для организации кода в приложениях и библиотеках. Они обеспечивают явность зависимостей, лучшую инкапсуляцию, поддержку Tree Shaking и являются частью веб-стандартов. Всегда отдавайте предпочтение модулям для новой разработки.

Выбор между ними сводится к контексту проекта и целевой среде выполнения, но в подавляющем большинстве случаев модули будут вашим выбором по умолчанию.