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.
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
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 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.
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 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.
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 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.
namespace Validation {
export interface Validator {
isValid(value: string): boolean;
}
}
/// <reference path="validation.ts" />
namespace Validation {
export class ZipCodeValidator implements Validator {
isValid(value: string): boolean {
return /^\d{3} ?\d{2}$/.test(value);
}
}
}
/// <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.
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²";
}
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.
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
- Prefer Modules: Use
importandexportto organize new application code. - Export Only the Public API: Keep helpers inside a namespace unexported, so that they stay private.
- Use Namespaces for Declarations: They remain useful to group types in
.d.tsfiles and to describe libraries with nested APIs. - Merge Deliberately: Merging with functions and classes is powerful but surprising; document why it is used.
- Check Your Tooling: Namespaces with values need a real compiler; tools that only strip types, and the
erasableSyntaxOnlyoption, reject them.
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
List all TypeScript tutorials.