Namespaces

Namespaces are TypeScript's original way of grouping related code under one name, from before JavaScript had modules. You'll rarely write new ones today, but you'll meet them in older codebases and type definition files, so it's worth knowing how they work.

Declaring a namespace

TS namespace.ts
namespace Validation {
  const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

  export function isEmail(value: string): boolean {
    return emailPattern.test(value);
  }

  export function isPostcode(value: string): boolean {
    return /^\d{5}$/.test(value);
  }
}

console.log(Validation.isEmail("a@b.co"));
console.log(Validation.isPostcode("1234"));
Output
true
false

Only members marked export are visible outside the namespace — emailPattern is private to it. You access exported members with dot notation, like properties on an object.

What a namespace compiles to

Unlike most TypeScript constructs, a namespace produces real JavaScript — an object built inside an immediately invoked function:

JS namespace.js (compiled, simplified)
var Validation;
(function (Validation) {
    const emailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
    function isEmail(value) { return emailPattern.test(value); }
    Validation.isEmail = isEmail;
    // ...
})(Validation || (Validation = {}));

This "IIFE" pattern was the standard way to avoid polluting the global scope before ES modules existed.

Nested namespaces and aliases

TS nested.ts
namespace App {
  export namespace Models {
    export interface Task {
      title: string;
      done: boolean;
    }
  }

  export namespace Utils {
    export const summary = (tasks: Models.Task[]) =>
      `${tasks.filter(t => t.done).length}/${tasks.length} done`;
  }
}

import Task = App.Models.Task;

const tasks: Task[] = [
  { title: "plan", done: true },
  { title: "build", done: false },
];
console.log(App.Utils.summary(tasks));
Output
1/2 done

import Task = App.Models.Task is a namespace alias — a shortcut name, not a module import. It's unique to namespaces.

Namespaces vs modules

NamespacesES modules
ScopeA named object (often global)One file
DependenciesImplicit, by load orderExplicit import statements
Tree-shaking by bundlersPoorGood
Standard JavaScript?No — TypeScript-onlyYes

For new code, use modules. Each file already acts as its own namespace, so wrapping a module's contents in a namespace just adds an extra layer to type.

Where namespaces still appear

Declaration files for global libraries

Libraries that attach themselves to a global variable (older jQuery plugins, analytics snippets, Google Maps' google.maps) are often described with a namespace in a .d.ts file:

D.TS analytics.d.ts
declare namespace Analytics {
  interface Event {
    name: string;
    props?: Record<string, string | number>;
  }
  function track(event: Event): void;
}

The declare keyword means "this exists at runtime somewhere else — I'm only describing it". Lesson 33 covers declaration files in detail.

Grouping types next to a function or class

A namespace with the same name as a function or class merges with it (lesson 34), which some libraries use to attach related types:

TS merge.ts
function format(value: number, options: format.Options = {}) {
  return value.toFixed(options.decimals ?? 2);
}

namespace format {
  export interface Options {
    decimals?: number;
  }
}

const opts: format.Options = { decimals: 1 };
console.log(format(3.14159, opts));
Output
3.1
Note: because they generate code, non-declare namespaces are not supported by tools that only strip types, such as Node's built-in TypeScript support or the erasableSyntaxOnly compiler option (lesson 44). That's one more reason modern code avoids them.