DEV Community

loading...
Cover image for TypeScript: Namespace declaration merging for organizing types

TypeScript: Namespace declaration merging for organizing types

Eddy Wilson
418 I'm a teapot
・3 min read

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:

// 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,
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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,
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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>
> = {}
Enter fullscreen mode Exit fullscreen mode

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:

// 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)
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)