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

Namespaces

namespace, declare namespace, объединение объявлений

📖 Введение

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

Namespaces (пространства имён) работают так же — они группируют связанные имена в отдельные «отделы». Это помогает избежать конфликтов имён и делает код более организованным.

Важно: Namespaces — это устаревший механизм TypeScript. В современном коде используются ES-модули (import/export). Но namespaces до сих пор встречаются в старых проектах, в DefinitelyTyped (репозитории типов для JavaScript-библиотек), и иногда в declaration файлах. Поэтому важно понимать, как они работают.

1 Зачем нужны namespaces — проблема конфликтов

Без namespaces (или модулей) все имена живут в одной глобальной области. Это приводит к конфликтам:

// utils.ts
function formatDate(date: Date): string {
  return date.toLocaleDateString('ru-RU');
}

// validators.ts
function formatDate(input: string): Date {
  return new Date(input);
}

Две функции formatDate — конфликт! Что если оба файла подключены к одной странице? Одна перезапишет другую.

Namespace решает эту проблему, создавая изолированную область:

// utils.ts
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString('ru-RU');
  }
}

// validators.ts
namespace Validators {
  export function formatDate(input: string): Date {
    return new Date(input);
  }
}

// Теперь конфликта нет:
Utils.formatDate(new Date());        // Работает
Validators.formatDate('2024-01-01'); // Работает

2 Объявление namespace — синтаксис и структура

Namespace объявляется с помощью ключевого слова namespace:

namespace MyNamespace {
  // Всё внутри — приватно по умолчанию
  const secret = 'я не доступен снаружи';

  // Экспортируем то, что нужно наружу
  export function publicFunction() {
    console.log(secret); // Могу использовать приватные переменные
  }

  export interface User {
    name: string;
    age: number;
  }
}

Ключевые правила:

  • Всё внутри namespace приватно по умолчанию
  • Чтобы сделать что-то доступным, добавьте export
  • Namespace может содержать функции, классы, интерфейсы, типы, константы
  • Имена namespace принято писать в PascalCase
  • Namespace могут быть вложенными

3 Вложенность namespace — иерархическая структура

Namespace могут быть вложенными друг в друга, создавая иерархическую структуру:

namespace App {
  export namespace Models {
    export interface User {
      id: number;
      name: string;
    }

    export interface Product {
      id: number;
      title: string;
      price: number;
    }
  }

  export namespace Services {
    export class UserService {
      getUser(): Models.User {
        return { id: 1, name: 'Алексей' };
      }
    }

    export class ProductService {
      getProduct(): Models.Product {
        return { id: 1, title: 'Ноутбук', price: 50000 };
      }
    }
  }
}

// Использование:
const service = new App.Services.UserService();
const user = service.getUser();

Или более компактный синтаксис с точечной нотацией:

namespace App.Models {
  export interface User {
    id: number;
    name: string;
  }
}

namespace App.Services {
  export class UserService {
    getUser(): App.Models.User {
      return { id: 1, name: 'Алексей' };
    }
  }
}

Обратите внимание: при точечной нотации родительский namespace не нужен — TypeScript создаёт его автоматически.

4 Namespace в нескольких файлах — объединение

Один namespace может быть разбит на несколько файлов. TypeScript автоматически объединяет их:

// models/user.ts
namespace App.Models {
  export interface User {
    id: number;
    name: string;
  }
}

// models/product.ts
namespace App.Models {
  export interface Product {
    id: number;
    title: string;
  }
}

// services/user.ts
namespace App.Services {
  export class UserService {
    getUser(): App.Models.User {
      return { id: 1, name: 'Алексей' };
    }
  }
}

Все три файла вносят вклад в один namespace App. Чтобы TypeScript знал об этом, нужно подключить все файлы в tsconfig.json или использовать /// <reference>:

// services/user.ts
/// <reference path="../models/user.ts" />
/// <reference path="../models/product.ts" />

namespace App.Services {
  export class UserService {
    getUser(): App.Models.User {
      return { id: 1, name: 'Алексей' };
    }
  }
}

/// <reference path="..." /> — это тройной слэш-директива. Она говорит компилятору: «подключи этот файл перед компиляцией текущего».

В tsconfig.json нужно указать файлы:

{
  "include": ["src/**/*.ts"]
}

5 declare namespace — описание глобальных библиотек

declare namespace используется для описания существующих глобальных переменных, которые не были написаны на TypeScript. Это часто встречается в .d.ts файлах:

// Описание глобальной переменной jQuery
declare namespace jQuery {
  function ready(callback: () => void): void;
  function ajax(settings: any): void;

  interface JQuery {
    text(): string;
    text(value: string): JQuery;
    html(): string;
    html(value: string): JQuery;
    val(): string;
    val(value: string): JQuery;
    addClass(className: string): JQuery;
    removeClass(className: string): JQuery;
    on(event: string, handler: (event: Event) => void): JQuery;
  }

  function $(selector: string): JQuery;
}

// Теперь TypeScript знает о структуре jQuery
// Вы можете использовать jQuery.$('.my-element').text()
// с полным автодополнением

Другой пример — Node.js:

// Описание глобального объекта process
declare namespace NodeJS {
  interface Process {
    env: { [key: string]: string | undefined };
    exit(code?: number): void;
    argv: string[];
    cwd(): string;
  }
}

declare const process: NodeJS.Process;

6 Namespace vs Module — что выбрать

Вот сравнение двух подходов:

Namespaces:

  • Работают в рантайме — создают обычные JavaScript-объекты
  • Не поддерживают tree-shaking
  • Не требуют настройки сборщика
  • Устарели — не рекомендуются для нового кода
  • Встречаются в declaration файлах (.d.ts)
  • Просты для небольших проектов

ES-модули (import/export):

  • Стандарт JavaScript (ES2015+)
  • Поддерживают tree-shaking
  • Требуют сборщик (Webpack, Vite, esbuild)
  • Рекомендуются для всего нового кода
  • Явный экспорт/импорт — чёткая структура
  • Лучше для больших проектов

7 Namespace и JavaScript — как компилируется

Namespace компилируется в обычный JavaScript-объект. Вот как это выглядит:

// TypeScript
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString('ru-RU');
  }

  export const PI = 3.14159;
}

// Скомпилированный JavaScript:
var Utils;
(function (Utils) {
  function formatDate(date) {
    return date.toLocaleDateString('ru-RU');
  }
  Utils.formatDate = formatDate;
  Utils.PI = 3.14159;
})(Utils || (Utils = {}));

Namespace — это просто IIFE (Immediately Invoked Function Expression), который создаёт объект. Это работает везде — в браузере, в Node.js, без сборщика.

🛠 Практический пример: типизация глобальных объектов

Допустим, вы работаете с библиотекой, которая добавляет глобальный объект MyLib:

// mylib.d.ts
declare namespace MyLib {
  function init(config: Config): void;
  function getVersion(): string;

  interface Config {
    apiKey: string;
    debug: boolean;
    timeout: number;
  }

  class Logger {
    constructor(prefix: string);
    log(message: string): void;
    warn(message: string): void;
    error(message: string): void;
  }
}

// Теперь TypeScript знает о MyLib:
MyLib.init({ apiKey: 'abc', debug: true, timeout: 5000 });
const version = MyLib.getVersion();
const logger = new MyLib.Logger('App');
logger.log('Запущено');

8 Namespace и модули — как они работают вместе

Namespace и модули могут использоваться вместе, но это не рекомендуется:

// utils.ts
export namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }

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

// app.ts
import { MathUtils } from './utils';

console.log(MathUtils.add(1, 2));  // 3
console.log(MathUtils.subtract(5, 3));  // 2

Но лучше использовать модули без namespace. Модули уже создают изолированную область — namespace не нужен.

⚠️ Типичные ошибки при работе с namespaces

1. Забыли export:

namespace Utils {
  function formatDate(date: Date): string {  // ❌ Приватная!
    return date.toLocaleDateString();
  }
}

Utils.formatDate(new Date());  // Ошибка: formatDate не доступна

2. Путаница с файлами:

// ❌ Namespace в файле с модулями (если в tsconfig.json module != none)
export namespace Utils {
  export function formatDate() {}
}

// ✅ Если используете модули, не смешивайте с namespaces

3. Использование namespace в современном коде:

// ❌ Не рекомендуется — используйте модули
namespace Utils {
  export function formatDate() {}
}

// ✅ Рекомендуется — используйте модули
export function formatDate() {}

9 Миграция с namespace на модули — пошаговый процесс

Если вы работаете со старым проектом и хотите перейти с namespace на модули:

Шаг 1: Убедитесь, что в tsconfig.json включены модули:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "node"
  }
}

Шаг 2: Замените namespace на экспорт:

// Было:
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString();
  }
}

// Стало:
export function formatDate(date: Date): string {
  return date.toLocaleDateString();
}

Шаг 3: Обновите импорты:

// Было:
Utils.formatDate(new Date());

// Стало:
import { formatDate } from './utils';
formatDate(new Date());

10 Namespace в DefinitelyTyped — реальные примеры

Вот как namespace используется в DefinitelyTyped для описания jQuery:

// @types/jquery/index.d.ts
declare namespace jQuery {
  interface JQueryAjaxSettings {
    url?: string;
    method?: string;
    data?: any;
    success?: (data: any) => void;
    error?: (jqXHR: JQueryXHR, textStatus: string) => void;
  }

  interface JQuery {
    // Манипуляция DOM
    html(): string;
    html(htmlString: string): JQuery;
    text(): string;
    text(textString: string): JQuery;
    val(): string | number | string[];
    val(value: string | number | string[]): JQuery;

    // CSS
    css(propertyName: string): string;
    css(propertyName: string, value: string | number): JQuery;

    // Классы
    addClass(className: string): JQuery;
    removeClass(className: string): JQuery;
    toggleClass(className: string): JQuery;
    hasClass(className: string): boolean;

    // События
    on(events: string, handler: Function): JQuery;
    off(events: string, handler?: Function): JQuery;
    trigger(eventType: string): JQuery;

    // Анимация
    animate(properties: Record<string, number>, duration?: number): JQuery;
    fadeIn(duration?: number): JQuery;
    fadeOut(duration?: number): JQuery;
  }
}

// Глобальная функция jQuery
declare function jQuery(selector: string | Element | JQuery): jQuery.JQuery;

// Псевдоним $
declare const $: typeof jQuery;

Это позволяет использовать jQuery в TypeScript с полной типизацией и автодополнением.

11 Namespace и глобальные переменные — изоляция

Namespace помогают изолировать глобальные переменные, чтобы они не конфликтовали:

// lib-a.ts
namespace LibA {
  export function init() {
    console.log('LibA инициализирована');
  }
}

// lib-b.ts
namespace LibB {
  export function init() {
    console.log('LibB инициализирована');
  }
}

// Теперь можно вызывать обе функции без конфликтов:
LibA.init();  // LibA инициализирована
LibB.init();  // LibB инициализирована

12 Namespace и типы данных — определение типов

Namespace могут содержать типы и интерфейсы:

// types.ts
namespace AppTypes {
  export interface User {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user' | 'guest';
  }

  export interface Product {
    id: number;
    title: string;
    price: number;
    category: string;
  }

  export type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';

  export interface Order {
    id: number;
    userId: number;
    products: Product[];
    total: number;
    status: OrderStatus;
    createdAt: Date;
  }
}

// Использование типов:
function processUser(user: AppTypes.User): string {
  return `${user.name} (${user.role})`;
}

function getOrderStatus(order: AppTypes.Order): string {
  return order.status;
}

13 Namespace и классы — определение классов

Namespace могут содержать классы:

// database.ts
namespace Database {
  export class Connection {
    private isConnected: boolean = false;

    constructor(private host: string, private port: number) {}

    connect(): void {
      console.log(`Подключение к ${this.host}:${this.port}`);
      this.isConnected = true;
    }

    disconnect(): void {
      console.log('Отключение от базы данных');
      this.isConnected = false;
    }

    isAlive(): boolean {
      return this.isConnected;
    }
  }

  export class Query {
    constructor(private connection: Connection) {}

    execute(sql: string): any[] {
      if (!this.connection.isAlive()) {
        throw new Error('Нет подключения к базе данных');
      }
      console.log(`Выполнение запроса: ${sql}`);
      return [];
    }
  }
}

// Использование:
const conn = new Database.Connection('localhost', 5432);
conn.connect();
const query = new Database.Query(conn);
const results = query.execute('SELECT * FROM users');

14 Namespace и декораторы — использование с декораторами

Namespace могут использоваться с декораторами для расширения функциональности:

// decorators.ts
namespace Decorators {
  export function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      console.log(`Вызов ${propertyKey} с аргументами:`, args);
      const result = originalMethod.apply(this, args);
      console.log(`Результат ${propertyKey}:`, result);
      return result;
    };
  }

  export function Measure(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const start = performance.now();
      const result = originalMethod.apply(this, args);
      const end = performance.now();
      console.log(`${propertyKey} выполнился за ${end - start}ms`);
      return result;
    };
  }
}

// Использование:
class Calculator {
  @Decorators.Log
  add(a: number, b: number): number {
    return a + b;
  }

  @Decorators.Measure
  complexCalculation(): number {
    // Тяжёлые вычисления
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return sum;
  }
}

15 Namespace и итераторы — перебор коллекций

Namespace могут содержать итераторы для удобного перебора коллекций:

// iterators.ts
namespace Iterators {
  export class Range {
    constructor(private start: number, private end: number, private step: number = 1) {}

    [Symbol.iterator](): Iterator<number> {
      let current = this.start;
      const end = this.end;
      const step = this.step;

      return {
        next(): IteratorResult<number> {
          if (current <= end) {
            const value = current;
            current += step;
            return { value, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  }

  export class Fibonacci {
    private prev: number = 0;
    private curr: number = 1;

    [Symbol.iterator](): Iterator<number> {
      return {
        next: (): IteratorResult<number> => {
          const value = this.curr;
          [this.prev, this.curr] = [this.curr, this.prev + this.curr];
          return { value, done: false };
        }
      };
    }
  }
}

// Использование:
const range = new Iterators.Range(1, 10, 2);
for (const num of range) {
  console.log(num);  // 1, 3, 5, 7, 9
}

const fib = new Iterators.Fibonacci();
let count = 0;
for (const num of fib) {
  console.log(num);  // 1, 1, 2, 3, 5, 8, 13, ...
  if (++count >= 10) break;
}

16 Namespace и утилиты — библиотека утилит

Namespace идеально подходят для создания библиотек утилит:

// utils.ts
namespace Utils {
  export function capitalize(str: string): string {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }

  export function camelCase(str: string): string {
    return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
  }

  export function slugify(str: string): string {
    return str
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/(^-|-$)/g, '');
  }

  export function truncate(str: string, length: number): string {
    if (str.length <= length) return str;
    return str.slice(0, length) + '...';
  }

  export function debounce<T extends (...args: any[]) => any>(
    fn: T,
    delay: number
  ): T {
    let timeoutId: ReturnType<typeof setTimeout>;
    return ((...args: any[]) => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => fn(...args), delay);
    }) as T;
  }
}

// Использование:
Utils.capitalize('hello');  // Hello
Utils.camelCase('my-long-name');  // myLongName
Utils.slugify('Hello World!');  // hello-world
Utils.truncate('Длинный текст', 10);  // Длинный ...

✅ Итого

  • namespace — механизм группировки имён, создаёт изолированную область
  • Всё приватно по умолчанию — используйте export для доступа снаружи
  • declare namespace — описание существующих глобальных объектов (часто в .d.ts файлах)
  • Namespace могут быть вложенными и разбиты на несколько файлов
  • Устаревший механизм — для нового кода используйте ES-модули (import/export)
  • Встречается в DefinitelyTyped и старых проектах

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

5 вопросов

Создание namespace

Premium

Вложенные namespace

Premium