TypeScript: Namespace declaration merging for organizing types
Tags: typescript
Published: 2021-07-25 10:18:03

## Introduction

Declaration merging is a concept in TypeScript which means the TS compiler will merge two or more separate declarations – with the same name – into one single declaration.

## Ambient namespace declaration

A TypeScript `namespace` allows you to access its exported values or types using *dot-notation*. While it's not recommended to use `namespace` in today's code (hello, not standard EcmaScript), it's still recommended and useful for describing and organizing types.

An *ambient* namespace declaration is a fully erasable (at compile time) namespace declaration. In other words, it doesn't emit any code.

## How is this useful?

An ambient namespace declaration can be merged with type aliases, interfaces, classes, and even functions. This allows you to access namespace's exported types using dot notation which is useful for organizing code.

## How to do it?

Append the `declare` keyword in front of the `namespace` keyword to make it an ambient declaration. Then use the same name of the entity (interface / type / class / ...) – that you want to merge with – as the namespace name. Look at the following examples:

### Organizing sub types and interfaces

Instead of exporting `User` and `UserDetails` interfaces, we can just define a single `User` interface and use ambient namespace declaration to export sub types:

```ts
// example.ts
interface User {
  FirstName: string
  LastName: string
  Details: User.Details
}
declare namespace User {
  interface Details {
    Address: string
  }
}
const details: User.Details = {
  Address: "Somewhere over the rainbow"
}
const user: User = {
  FirstName: "Bob",
  LastName: "Alice",
  Details: details,
}
```

Note that because we're using `declare` keyword, there is no need to `export` the `Details` interface (auto-exported).

### Exposing interface property types

Sometimes it's useful to expose interface's property types. For instance, branded types:

```ts
// example.ts
interface User {
  FirstName: User.FirstName
  LastName: User.LastName
}
declare namespace User {
  type FirstName = string & { __brand: "User:FirstName" }
  type LastName = string & { __brand: "User:LastName" }
}

const firstName = <User.FirstName>"Bob"
const lastName = <User.LastName>"Alice"
const user: User = {
  FirstName: firstName,
  LastName: lastName,
}
```

> **In .tsx files:** use `"Bob" as User.FirstName` and `"Alice" as User.LastName`

### Exposing an interface's default type params

It's currently not possible in TypeScript to have partial type params, you either need to provide none or all of them. However, here is a way you can make it easier to access the default type params of an interface you have declared:

```ts
// example.ts
interface Foo<
  T = Foo.T,
  V = Foo.V,
  S = Foo.S<T>,
> {
  /** property types here */
}
declare namespace Foo {
  type T = string
  type V = string
  type S<T> = { t: T, something: string }
}

const foo: Foo<
  Foo.T,
  Foo.V,
  Foo.S<Foo.T>
> = {}
```

### Scoped types in object literals

Sometimes we define object literals which may contain some methods. To prevent polluting the module scope with types – because naming things is hard, even more if there is already an existing type with the same name. e.g: how common is `Callback` – we could use ambient namespaces:

```ts
// example.ts
const foo = <const>{
  trigger(cb: foo.Callback): void { }
}
declare namespace foo {
  interface Callback {
    (value: string): void
  }
}

const myCallback: foo.Callback = (value: string): void => {
  return void console.log(value)
}

foo.trigger(myCallback)
```

> This also applies to classes, enums, and functions.

## Conclusion

* namespaces allow accessing its exported types and values using dot-notation
* ambient namespaces are fully erasable types (do not emit code)
* namespaces and ambient namespaces can be merged with other declarations in the same scope such as type aliases, interfaces, object literals, classes, enums, functions, and even other namespaces
* ambient namespaces are useful for organizing or scoping types

## More

* [TypeScript Namespaces](https://www.typescriptlang.org/docs/handbook/namespaces.html)
* [TypeScript Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)