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

Пространства имён в TypeScript

Пространство имён — это именованный контейнер для связанного кода. Оно объединяет типы, функции и классы под одним именем, чтобы они не конфликтовали с другим кодом.


Как устроены пространства имён в TypeScript

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

Они помогают структурировать большие кодовые базы и держать область видимости под контролем — чисто и удобно для сопровождения.

Ключевые понятия:

  • Логическая группировка: объединение связанного кода в именованные контейнеры
  • Управление областью видимости: контроль того, какие части кода видны снаружи
  • Предотвращение конфликтов имён: отсутствие конфликтов между сущностями с похожими именами
  • Организация кода: построение иерархии в больших приложениях

Когда использовать пространства имён:

  • Организация кода в больших устаревших приложениях
  • Работа с глобальными библиотеками
  • Миграция со старых кодовых баз на JavaScript
  • Работа с кодом, который должен быть доступен глобально

Примечание: пространства имён по-прежнему полностью поддерживаются, но в современных приложениях обычно используют ES-модули (import/export), потому что модули дают лучшую модульность и позволяют сборщикам удалять неиспользуемый код (tree-shaking). Тем не менее понимание пространств имён полезно для сопровождения устаревших кодовых баз и в некоторых сценариях разработки библиотек.


Базовый синтаксис пространства имён

Пространство имён объявляется ключевым словом namespace, за которым следует блок. Снаружи блока доступны только члены, помеченные export; всё остальное остаётся приватным для пространства имён.

Пример

Пространство имён Validation экспортирует интерфейс и два класса, а lettersRegexp остаётся приватным для него. Снаружи к классам обращаются через точечную запись: Validation.LettersValidator.

Prompt
Покажите базовый синтаксис namespace с экспортированными и приватными членами. Объявите пространство имён Validation, которое экспортирует интерфейс StringValidator (isValid(s: string): boolean), содержит неэкспортированную константу lettersRegexp (/^[A-Za-z]+$/) и экспортирует два класса, реализующих этот интерфейс: LettersValidator (только буквы) и ZipCodeValidator (только цифры и ровно пять символов). Вне пространства имён создайте по одному объекту каждого класса через Validation.<Class> и выведите результат isValid для "Hello", "Hello123", "12345" и "1234".
Вывод: программа на TypeScript и захваченный вывод

Продвинутые возможности пространств имён

Пространства имён можно вкладывать друг в друга, выстраивая иерархию, а длинные пути к ним можно сокращать с помощью псевдонимов.

Вложенные пространства имён

App содержит два экспортированных внутренних пространства имён: Utils и Models. К членам обращаются по полному пути, например App.Utils.log; запись App.log была бы ошибкой, потому что log не является прямым членом App.

Prompt
Покажите вложенные пространства имён, к которым обращаются через точечную запись. Объявите пространство имён App, содержащее экспортированное пространство имён Utils (с функциями log и error, которые выводят [LOG]: msg и [ERROR]: msg) и экспортированное пространство имён Models (интерфейс User с полями id, name, email и класс UserService, метод getUser(id) которого возвращает John Doe / [email protected]). Вызовите App.Utils.log("Запуск приложения"), загрузите пользователя 1 через App.Models.UserService, выведите Пользователь загружен: <name> и оставьте закомментированный вызов App.log(...) с пометкой, что он был бы ошибкой.
Вывод: программа на TypeScript и захваченный вывод

Псевдонимы пространств имён

Для пространства имён или для одного из его членов можно создать псевдоним с помощью import Alias = Some.Long.Path. Это не импорт модуля — так длинному имени просто даётся короткое. Components — псевдоним самого внутреннего пространства имён, а Button — псевдоним одного класса.

Prompt
Покажите псевдонимы пространств имён, создаваемые с помощью import Alias = Some.Long.Path. Объявите VeryLongNamespace.DeeplyNested.Components (три вложенных пространства имён), экспортирующие классы Button и TextField, методы display() которых выводят Button отображён и TextField отображён. Создайте и отобразите кнопку тремя способами: через полный путь, через псевдоним Components самого внутреннего пространства имён и через псевдоним Button самого класса.
Вывод: программа на TypeScript и захваченный вывод

Пространства имён из нескольких файлов

В больших приложениях код часто разделён на несколько файлов. Пространство имён TypeScript может быть распределено по нескольким файлам и объединено во время компиляции с помощью комментариев-ссылок.

Комментарий-ссылка вида /// <reference path="validators.ts" /> сообщает TypeScript, что один файл зависит от другого, поэтому компилятор знает, в каком порядке их объединять. Четыре файла ниже вместе образуют одно пространство имён Validation.

Чтобы скомпилировать эти файлы в один файл JavaScript, выполните tsc --outFile sample.js main.ts.

validators.ts

Первый файл объявляет пространство имён Validation и экспортирует интерфейс StringValidator.

script.ts
namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

letters-validator.ts

Этот файл ссылается на validators.ts и дополняет то же пространство имён Validation классом LettersValidator.

script.ts
/// <reference path="validators.ts" />
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

zipcode-validator.ts

Этот файл тоже ссылается на validators.ts и добавляет в пространство имён класс ZipCodeValidator.

script.ts
/// <reference path="validators.ts" />
namespace Validation {
  const zipCodeRegexp = /^[0-9]+$/;

  export class ZipCodeValidator implements StringValidator {
    isValid(s: string): boolean {
      return zipCodeRegexp.test(s) && s.length === 5;
    }
  }
}

main.ts

Главный файл ссылается на все три файла, создаёт оба валидатора и выводит, подходит ли каждая проверяемая строка под каждый валидатор.

script.ts
/// <reference path="validators.ts" />
/// <reference path="letters-validator.ts" />
/// <reference path="zipcode-validator.ts" />

// Now you can use the validators from multiple files
let validators: { [s: string]: Validation.StringValidator } = {};
validators["letters"] = new Validation.LettersValidator();
validators["zipcode"] = new Validation.ZipCodeValidator();

// Some samples to validate
let strings = ["Hello", "98052", "101"];

// Validate each
strings.forEach(s => {
  for (let name in validators) {
    console.log(`"${s}" - ${validators[name].isValid(s) ? "matches" : "does not match"} ${name}`);
  }
});

Пространства имён и модули

Важно понимать, когда использовать пространства имён, а когда — ES-модули:

  • Модули — предпочтительный способ организации кода в современных приложениях на TypeScript
  • Пространства имён по-прежнему полезны в отдельных случаях, например при слиянии объявлений или работе с устаревшим кодом
  • Модули лучше поддерживаются инструментами и допускают tree-shaking
  • Пространства имён удобны для создания глобальных библиотек

Ниже — сравнение пространств имён и ES-модулей (import/export) по отдельным характеристикам:

  • Рекомендуемая область применения и масштаб — Пространства имён: простые конфигурации, небольшие приложения, устаревшие кодовые базы. Модули: современные приложения любого размера; предпочтительны для новых проектов.
  • Синтаксис и использование — Пространства имён: глобальный доступ через точечную запись (например, MyNS.Member). Модули: явные import/export с путями к файлам.
  • Загрузка и сборка — Пространства имён: загрузчик не нужен; можно получить один файл с помощью --outFile. Модули: обычно проходят через сборщик или загрузчик (Vite, webpack и т. д.).
  • Разделение по файлам — Пространства имён: возможно с помощью комментариев /// <reference />. Модули: естественно; каждый файл является модулем с явными экспортами и импортами.
  • Tree-shaking — Пространства имён: ограниченно; сборщикам трудно удалять неиспользуемый код. Модули: отлично; они и созданы для удаления мёртвого кода.
  • Глобальная область видимости — Пространства имён: поощряют глобальные переменные (пусть и внутри пространства имён). Модули: избегают глобальных переменных; зависимости объявляются явно.
  • Дополнение и слияние — Пространства имён: хорошая поддержка через слияние объявлений. Модули: дополнение модуля возможно, но с большими ограничениями.
  • Инструменты и экосистема — Пространства имён: работают, но хуже согласуются с современными инструментами. Модули: лучшая поддержка со стороны современных инструментов и платформ.
  • Лучше всего подходят для — Пространства имён: устаревшие библиотеки, глобальные скрипты, окружающие (ambient) типы. Модули: вся новая разработка, библиотеки и приложения.

Продвинутые приёмы работы с пространствами имён

С пространствами имён часто применяют ещё два приёма: дополнение существующего пространства имён через слияние объявлений и размещение внутри него обобщённых типов и классов.

Дополнение пространства имён

Второй блок declare namespace Express сливается с первым, добавляя requestTime и log в Request, а также новый интерфейс UserSession; код использования предполагает, что функция express() приходит из библиотеки.

script.ts
// Original namespace
declare namespace Express {
  interface Request {
    user?: { id: number; name: string };
  }
  interface Response {
    json(data: any): void;
  }
}

// Later in your application (e.g., in a .d.ts file)
declare namespace Express {
  // Augment the Request interface
  interface Request {
    // Add custom properties
    requestTime?: number;
    // Add methods
    log(message: string): void;
  }

  // Add new types
  interface UserSession {
    userId: number;
    expires: Date;
  }
}

// Usage in your application
const app = express();

app.use((req: Express.Request, res: Express.Response, next) => {
  // Augmented properties and methods are available
  req.requestTime = Date.now();
  req.log('Request started');
  next();
});

Пространства имён с дженериками

Пространство имён DataStorage экспортирует обобщённый интерфейс Repository<T> и реализующий его класс InMemoryRepository<T>, который здесь используется с типом User.

script.ts
// Generic namespace example
namespace DataStorage {
  export interface Repository<T> {
    getAll(): T[];
    getById(id: number): T | undefined;
    add(item: T): void;
    update(id: number, item: T): boolean;
    delete(id: number): boolean;
  }

  // Concrete implementation
  export class InMemoryRepository<T> implements Repository<T> {
    private items: T[] = [];

    getAll(): T[] {
      return [...this.items];
    }

    getById(id: number): T | undefined {
      return this.items[id];
    }

    add(item: T): void {
      this.items.push(item);
    }

    update(id: number, item: T): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items[id] = item;
        return true;
      }
      return false;
    }

    delete(id: number): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items.splice(id, 1);
        return true;
      }
      return false;
    }
  }
}

// Usage
interface User {
  id: number;
  name: string;
  email: string;
}

const userRepo = new DataStorage.InMemoryRepository<User>();
userRepo.add({ id: 1, name: 'John Doe', email: '[email protected]' });
const allUsers = userRepo.getAll();

Рекомендации

Делайте так:

  • Используйте осмысленные иерархические имена пространств имён
  • Экспортируйте из пространства имён только то, что действительно нужно
  • Используйте комментарии /// <reference /> для упорядочивания файлов в пространствах имён из нескольких файлов
  • Рассматривайте модули для новых проектов
  • Используйте const enum внутри пространств имён для лучшей производительности
  • Документируйте пространства имён комментариями JSDoc

Так не делайте:

  • Не создавайте слишком глубокие иерархии пространств имён (более 2–3 уровней)
  • Не засоряйте глобальную область видимости без необходимости
  • Не смешивайте пространства имён и модули в одном проекте без чёткой стратегии
  • Не используйте пространства имён в небольших приложениях — предпочитайте модули

Что учитывать с точки зрения производительности:

  • Большие пространства имён могут увеличивать размер сборки
  • Для больших приложений рассмотрите разделение кода на части
  • Следите за циклическими зависимостями в сложных структурах пространств имён
  • Используйте const enum для лучшей производительности с константными значениями

Переход от пространств имён к модулям

Переход от пространств имён к ES-модулям обычно означает, что каждое пространство имён превращается в отдельный файл с членами, помеченными export, а обращение через точку заменяется инструкциями import.

Шаги миграции:

  1. Превратите каждое пространство имён в файл-модуль
  2. Замените export внутри пространства имён на экспорт ES-модуля
  3. Обновите импорты, чтобы они использовали синтаксис ES-модулей
  4. Настройте систему сборки для работы с модулями
  5. Обновите тесты под новую структуру модулей
  6. Рассмотрите использование сборщика, например webpack или Rollup
  7. Обновите tsconfig.json, указав "module": "ESNext"

Инструменты для миграции:

  • ts-migrate — инструмент автоматической миграции от Facebook
  • tslint с правилом no-namespace для обнаружения пространств имён
  • Встроенные инструменты рефакторинга TypeScript

До и после миграции

Один и тот же класс UserService сначала находится внутри вложенных пространств имён, а затем становится экспортированным классом в собственном файле-модуле, который импортируется там, где нужен.

script.ts
// Before: Using namespaces
namespace MyApp {
  export namespace Services {
    export class UserService {
      getUser(id: number) { /* ... */ }
    }
  }
}

// After: Using ES modules
// services/UserService.ts
export class UserService {
  getUser(id: number) { /* ... */ }
}

// app.ts
import { UserService } from './services/UserService';
const userService = new UserService();