Was this page helpful?

Deep Dive

Declaration File Theory: A Deep Dive

Structuring modules to give the exact API shape you want can be tricky.For example, we might want a module that can be invoked with or withoutnew to produce different types,has a variety of named types exposed in a hierarchy,and has some properties on the module object as well.

By reading this guide, you’ll have the tools to write complex declaration files that expose a friendly API surface.This guide focuses on module (or UMD) libraries because the options here are more varied.

Key Concepts

You can fully understand how to make any shape of declarationby understanding some key concepts of how TypeScript works.

Types

If you’re reading this guide, you probably already roughly know what a type in TypeScript is.To be more explicit, though, atype is introduced with:

  • A type alias declaration (type sn = number | string;)
  • An interface declaration (interface I { x: number[]; })
  • A class declaration (class C { })
  • An enum declaration (enum E { A, B, C })
  • Animport declaration which refers to a type

Each of these declaration forms creates a new type name.

Values

As with types, you probably already understand what a value is.Values are runtime names that we can reference in expressions.For examplelet x = 5; creates a value calledx.

Again, being explicit, the following things create values:

  • let,const, andvar declarations
  • Anamespace ormodule declaration which contains a value
  • Anenum declaration
  • Aclass declaration
  • Animport declaration which refers to a value
  • Afunction declaration

Namespaces

Types can exist innamespaces.For example, if we have the declarationlet x: A.B.C,we say that the typeC comes from theA.B namespace.

This distinction is subtle and important — here,A.B is not necessarily a type or a value.

Simple Combinations: One name, multiple meanings

Given a nameA, we might find up to three different meanings forA: a type, a value or a namespace.How the name is interpreted depends on the context in which it is used.For example, in the declarationlet m: A.A = A;,A is used first as a namespace, then as a type name, then as a value.These meanings might end up referring to entirely different declarations!

This may seem confusing, but it’s actually very convenient as long as we don’t excessively overload things.Let’s look at some useful aspects of this combining behavior.

Built-in Combinations

Astute readers will notice that, for example,class appeared in both thetype andvalue lists.The declarationclass C { } creates two things:atypeC which refers to the instance shape of the class,and avalueC which refers to the constructor function of the class.Enum declarations behave similarly.

User Combinations

Let’s say we wrote a module filefoo.d.ts:

ts
exportvarSomeVar: {a:SomeType };
exportinterfaceSomeType {
count:number;
}

Then consumed it:

ts
import*asfoofrom"./foo";
letx:foo.SomeType =foo.SomeVar.a;
console.log(x.count);

This works well enough, but we might imagine thatSomeType andSomeVar were very closely relatedsuch that you’d like them to have the same name.We can use combining to present these two different objects (the value and the type) under the same nameBar:

ts
exportvarBar: {a:Bar };
exportinterfaceBar {
count:number;
}

This presents a very good opportunity for destructuring in the consuming code:

ts
import {Bar }from"./foo";
letx:Bar =Bar.a;
console.log(x.count);

Again, we’ve usedBar as both a type and a value here.Note that we didn’t have to declare theBar value as being of theBar type — they’re independent.

Advanced Combinations

Some kinds of declarations can be combined across multiple declarations.For example,class C { } andinterface C { } can co-exist and both contribute properties to theC types.

This is legal as long as it does not create a conflict.A general rule of thumb is that values always conflict with other values of the same name unless they are declared asnamespaces,types will conflict if they are declared with a type alias declaration (type s = string),and namespaces never conflict.

Let’s see how this can be used.

Adding using aninterface

We can add additional members to aninterface with anotherinterface declaration:

ts
interfaceFoo {
x:number;
}
// ... elsewhere ...
interfaceFoo {
y:number;
}
leta:Foo = ...;
console.log(a.x +a.y);// OK

This also works with classes:

ts
classFoo {
x:number;
}
// ... elsewhere ...
interfaceFoo {
y:number;
}
leta:Foo = ...;
console.log(a.x +a.y);// OK

Note that we cannot add to type aliases (type s = string;) using an interface.

Adding using anamespace

Anamespace declaration can be used to add new types, values, and namespaces in any way which does not create a conflict.

For example, we can add a static member to a class:

ts
classC {}
// ... elsewhere ...
namespaceC {
exportletx:number;
}
lety =C.x;// OK

Note that in this example, we added a value to thestatic side ofC (its constructor function).This is because we added avalue, and the container for all values is another value(types are contained by namespaces, and namespaces are contained by other namespaces).

We could also add a namespaced type to a class:

ts
classC {}
// ... elsewhere ...
namespaceC {
exportinterfaceD {}
}
lety:C.D;// OK

In this example, there wasn’t a namespaceC until we wrote thenamespace declaration for it.The meaningC as a namespace doesn’t conflict with the value or type meanings ofC created by the class.

Finally, we could perform many different merges usingnamespace declarations.This isn’t a particularly realistic example, but shows all sorts of interesting behavior:

ts
namespaceX {
exportinterfaceY {}
exportclassZ {}
}
// ... elsewhere ...
namespaceX {
exportvarY:number;
exportnamespaceZ {
exportclassC {}
}
}
typeX =string;

In this example, the first block creates the following name meanings:

  • A valueX (because thenamespace declaration contains a value,Z)
  • A namespaceX (because thenamespace declaration contains a type,Y)
  • A typeY in theX namespace
  • A typeZ in theX namespace (the instance shape of the class)
  • A valueZ that is a property of theX value (the constructor function of the class)

The second block creates the following name meanings:

  • A valueY (of typenumber) that is a property of theX value
  • A namespaceZ
  • A valueZ that is a property of theX value
  • A typeC in theX.Z namespace
  • A valueC that is a property of theX.Z value
  • A typeX

The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request

Contributors to this page:
MHMohamed Hegazy  (54)
OTOrta Therox  (12)
1+

Last updated: Oct 06, 2025