Avatar ·

Как правильно использовать namespace вместе с внешними модулями в TypeScript?

Привет! Я изучаю TypeScript и пытаюсь структурировать свой проект. Раньше я использовал namespace (пространства имён) для организации кода внутри одного файла, но теперь, когда проект вырос, я хочу разбить код на отдельные модули (файлы). Проблема в том, что когда я пытаюсь импортировать модуль, который содержит namespace, я получаю ошибки или namespace не работает так, как я ожидаю. Вот пример того, что я делаю: У меня есть файл `math.ts`: ```typescript 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`: ```typescript import { MathUtils } from './math'; console.log(MathUtils.add(5, 3)); // Ошибка: MathUtils не экспортируется из модуля ``` Я получаю ошибку, что MathUtils не экспортируется. Если я добавляю `export` перед `namespace`, то получаю другую ошибку: "Namespace 'MathUtils' cannot be used as a type". Подскажите, как правильно организовать код, чтобы использовать преимущества namespace (группировка функций/классов) в проекте, где код разбит на модули? Может быть, я вообще неправильно подхожу к архитектуре? Заранее спасибо! <<>>

Понимание разницы между namespace и модулями в TypeScript

Вы столкнулись с одной из самых частых проблем при переходе от глобального кода к модульной системе в TypeScript. Давайте разберёмся.

Ключевая идея: В современном TypeScript (начиная с версии 1.5) namespace и module (внешние модули) — это разные концепции, и их не следует смешивать. Когда вы используете import/export в файле, он автоматически становится внешним модулем. Внутри внешнего модуля использование namespace теряет смысл, потому что модуль сам по себе уже обеспечивает изоляцию и группировку кода.

Правильные решения

Решение 1: Использовать модули без namespace (рекомендуется)

Самый простой и современный подход — просто экспортировать функции/классы напрямую из модуля, без обёртки в namespace:

// math.ts
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 { add, subtract } from './math';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

Если вы хотите сохранить группировку, можно использовать именованный импорт:

// app.ts
import * as MathUtils from './math';
console.log(MathUtils.add(5, 3));
console.log(MathUtils.subtract(5, 3));

Решение 2: Использовать namespace внутри модуля (легаси-подход)

Если по каким-то причинам вам нужно сохранить namespace (например, для обратной совместимости с глобальным кодом), можно сделать так:

// math.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 './math';
console.log(MathUtils.add(5, 3)); // Теперь работает!

Обратите внимание на export перед namespace. Это превращает namespace в экспортируемый объект, который можно импортировать.

Решение 3: Использовать объект-контейнер (альтернатива namespace)

Вместо namespace можно просто создать объект, который группирует функции:

// math.ts
export const MathUtils = {
    add(a: number, b: number): number {
        return a + b;
    },
    subtract(a: number, b: number): number {
        return a - b;
    }
};

// app.ts
import { MathUtils } from './math';
console.log(MathUtils.add(5, 3));

Когда использовать namespace?

  • Глобальный код — если вы пишете скрипт без модульной системы (без import/export).
  • Объявление типов для глобальных библиотек — в файлах .d.ts для описания глобальных переменных.
  • Внутренняя организация внутри одного файла — если файл очень большой и вы хотите логически сгруппировать код.

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

Для новых проектов используйте только модули (экспорт/импорт). Namespace — это пережиток времён, когда TypeScript не поддерживал модули ES6. Современный TypeScript (и JavaScript) ориентируется на модульную систему. Если вам нужна группировка — используйте либо именованные импорты, либо объекты-контейнеры.

<<>>

Дополнительный подход: Использование namespace для типов и интерфейсов

Если вы работаете с проектом, где namespace всё ещё используется для организации типов (например, в старом коде или при генерации Swagger/OpenAPI), вот как можно правильно импортировать типы из namespace, экспортированного из модуля:

// types.ts
export namespace ApiTypes {
    export interface User {
        id: number;
        name: string;
    }
    export type Status = 'active' | 'inactive';
}

// app.ts
import { ApiTypes } from './types';
const user: ApiTypes.User = { id: 1, name: 'Alice' };
const status: ApiTypes.Status = 'active';

Также можно использовать import type для импорта только типов:

// app.ts
import type { ApiTypes } from './types';
// теперь ApiTypes будет удалён из скомпилированного JavaScript

Но ещё раз подчеркну: для нового кода лучше вообще отказаться от namespace в пользу прямого экспорта типов:

// types.ts
export interface User {
    id: number;
    name: string;
}
export type Status = 'active' | 'inactive';

// app.ts
import type { User, Status } from './types';

Это чище, проще для понимания и лучше поддерживается инструментами (IDE, линтеры, tree-shaking).