ZetCode

TypeScript Namespaces

last modified October 15, 2026

A namespace groups related variables, functions, classes, and types under one name. It was TypeScript's way of organizing code before ECMAScript modules existed, and it was originally called an internal module. A namespace is compiled to a plain JavaScript object, so it exists at runtime. Today, ES modules are recommended for organizing application code, but namespaces are still common in declaration files for libraries, in older code bases, and for attaching extra members to functions and classes.

Creating a Namespace

Members of a namespace are private unless they are marked with export.

basic_namespace.ts
namespace Greetings {
    const defaultName = "stranger";

    export const version = "1.0";

    export function hello(name: string = defaultName): string {
        return `Hello, ${name}!`;
    }

    export function goodbye(name: string = defaultName): string {
        return `Goodbye, ${name}!`;
    }
}

console.log(Greetings.hello("Alice"));
console.log(Greetings.goodbye());
console.log(Greetings.version);

The exported members are accessed with the namespace name and a dot. The defaultName constant is not exported; the functions inside the namespace can use it, but code outside cannot.

$ npx tsx basic_namespace.ts
Hello, Alice!
Goodbye, stranger!
1.0
private_member.ts
namespace Greetings {
    const defaultName = "stranger";

    export function hello(name: string = defaultName): string {
        return `Hello, ${name}!`;
    }
}

console.log(Greetings.defaultName);

The compiler reports that defaultName does not exist on the namespace, because only exported members are visible from outside.

$ npx tsc --noEmit
private_member.ts(9,23): error TS2339: Property 'defaultName' does not exist on type 'typeof Greetings'.

Namespaces at Runtime

A namespace that contains values compiles to an object created by an immediately invoked function.

namespace_runtime.ts
namespace Counter {
    let count = 0;

    export function increment(): number {
        return ++count;
    }
}

Counter.increment();
Counter.increment();

console.log(typeof Counter);
console.log(Object.keys(Counter));
console.log(Counter.increment());

Counter is an ordinary object with the exported increment function as its property. The count variable lives in the closure of the generated function, so it keeps its state between calls but cannot be reached from outside.

Because namespaces generate code, they are not supported by tools that only strip types, such as Node.js running .ts files directly. The erasableSyntaxOnly compiler option reports them. A namespace that contains only types produces no JavaScript at all.

$ npx tsx namespace_runtime.ts
object
[ 'increment' ]
3

Nested Namespaces

Namespaces can be nested to build a hierarchy. A dotted name is a shorthand for nested declarations.

nested_namespace.ts
namespace App {
    export namespace Utils {
        export function capitalize(text: string): string {
            return text.charAt(0).toUpperCase() + text.slice(1);
        }
    }
}

namespace App.Models {
    export class User {
        constructor(public name: string) {}

        label(): string {
            return App.Utils.capitalize(this.name);
        }
    }
}

const user = new App.Models.User("alice");
console.log(user.label());
console.log(Object.keys(App));

namespace App.Models is the same as an App namespace containing an exported Models namespace. Both declarations contribute to the same App object, which therefore has the Utils and Models members. Inside Models, the full path App.Utils is used to reach the sibling namespace.

$ npx tsx nested_namespace.ts
Alice
[ 'Utils', 'Models' ]

Namespace Aliases

Long namespace paths can be shortened with an import alias.

namespace_alias.ts
namespace Company.Departments.Engineering {
    export const teams = ["platform", "mobile", "data"];

    export function headcount(team: string): number {
        return team.length * 3;
    }
}

import Eng = Company.Departments.Engineering;

for (const team of Eng.teams) {
    console.log(`${team}: ${Eng.headcount(team)} people`);
}

The import Eng = ... syntax creates a local alias for the nested namespace. Despite the import keyword, it has nothing to do with modules; it only gives a shorter name to an existing namespace. Like namespaces themselves, it is not supported when types are only stripped.

$ npx tsx namespace_alias.ts
platform: 24 people
mobile: 18 people
data: 12 people

Merging Namespaces

Several namespace declarations with the same name are merged into one.

merging_namespaces.ts
namespace Shapes {
    export function square(side: number): number {
        return side * side;
    }
}

namespace Shapes {
    export function circle(radius: number): number {
        return Math.PI * radius * radius;
    }

    export const all = [square(2), circle(1)];
}

console.log(Shapes.square(3));
console.log(Shapes.circle(2).toFixed(2));
console.log(Shapes.all.map(n => n.toFixed(2)));

The second declaration adds members to the first one. Exported members of one declaration are visible in the other, so all can call square directly. Non-exported members, however, are visible only inside their own declaration block.

$ npx tsx merging_namespaces.ts
9
12.57
[ '4.00', '3.14' ]

Merging with Functions and Classes

A namespace can be merged with a function or a class of the same name. This attaches extra properties to it, which is a common pattern in JavaScript libraries.

function_namespace.ts
function format(value: number): string {
    return `${value.toFixed(format.decimals)} ${format.unit}`;
}

namespace format {
    export let decimals = 2;
    export const unit = "EUR";
}

class Color {
    constructor(public r: number, public g: number, public b: number) {}

    toHex(): string {
        return "#" + [this.r, this.g, this.b].map(c => c.toString(16).padStart(2, "0")).join("");
    }
}

namespace Color {
    export const red = new Color(255, 0, 0);

    export function fromGray(level: number): Color {
        return new Color(level, level, level);
    }
}

console.log(format(19.987));
format.decimals = 0;
console.log(format(19.987));

console.log(Color.red.toHex(), Color.fromGray(128).toHex());

The format function gets two properties that configure it, and the Color class gets a constant and a factory function that act like static members. The function or class must come before the namespace in the file, because the namespace adds properties to an existing object. In modern code, static class members achieve the same for classes.

$ npx tsx function_namespace.ts
19.99 EUR
20 EUR
#ff0000 #808080

Splitting Namespaces Across Files

Without modules, a namespace can be spread over several files that reference each other with triple-slash directives.

validation.ts
namespace Validation {
    export interface Validator {
        isValid(value: string): boolean;
    }
}
zipCode.ts
/// <reference path="validation.ts" />
namespace Validation {
    export class ZipCodeValidator implements Validator {
        isValid(value: string): boolean {
            return /^\d{3} ?\d{2}$/.test(value);
        }
    }
}
app.ts
/// <reference path="zipCode.ts" />
const validator = new Validation.ZipCodeValidator();

for (const zip of ["811 02", "81102", "8110"]) {
    console.log(`${zip}: ${validator.isValid(zip)}`);
}

The /// <reference path="..." /> directive tells the compiler that a file depends on another one. The two Validation declarations in different files are merged, so ZipCodeValidator can implement the Validator interface. The --outFile option concatenates all referenced files, in dependency order, into one JavaScript file.

$ npx tsc --strict --target es2022 --outFile app.js app.ts
$ node app.js
811 02: true
81102: true
8110: false

The generated app.js contains the code of zipCode.ts and app.ts. The validation.ts file contributes nothing to it, because it declares only an interface. This approach relies on global names and on the correct order of files; ES modules handle both automatically, which is why they replaced it.

Namespaces in Modules

A namespace can be exported from a module like any other value.

geometry.ts
export namespace Geometry {
    export namespace Area {
        export function rectangle(width: number, height: number): number {
            return width * height;
        }

        export function circle(radius: number): number {
            return Math.PI * radius ** 2;
        }
    }

    export const UNIT = "cm²";
}
main.ts
import { Geometry } from "./geometry.js";

const { Area, UNIT } = Geometry;

console.log(`${Area.rectangle(3, 4)} ${UNIT}`);
console.log(`${Area.circle(1).toFixed(2)} ${UNIT}`);

Inside a module, a namespace only adds a level of nesting, because the module already has its own scope. The same structure is usually written as separate modules or as a module that exports plain functions; the modules tutorial shows how. Namespaces in modules are mostly found in declaration files that describe libraries with nested APIs.

$ npx tsx main.ts
12 cm²
3.14 cm²

Type-Only Namespaces

A namespace that contains only types is a way to group related type declarations. It is erased completely.

type_namespace.ts
namespace Api {
    export interface User {
        id: number;
        name: string;
    }

    export type Response<T> = { status: number; data: T };
}

function getUser(): Api.Response<Api.User> {
    return { status: 200, data: { id: 1, name: "Alice" } };
}

const response = getUser();
console.log(response.status, response.data.name);

The types are referred to as Api.User and Api.Response. Because the namespace contains no values, the compiler generates no Api object; using Api as a value, as in console.log(Api), would be an error saying that a namespace cannot be used as a value. This form is common in declaration files, where it groups the types of a library under one name.

$ npx tsx type_namespace.ts
200 Alice

Best Practices

Source

TypeScript Handbook: Namespaces

This tutorial covered namespaces in TypeScript. In new code, modules are the preferred way to organize code; namespaces remain useful in declaration files and when reading older code bases.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all TypeScript tutorials.