cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Declaration merging

See how interfaces with the same name join into one type, add your own property to the browser's Window, and learn why type aliases don't merge.

By the end, you can
  • 01
    Predict a merged interfaceSay which properties a type has when two interfaces share a name, and which clashes TypeScript reports.
  • 02
    Add to the browser's typesUse declare global and interface Window to add your own property, and type a custom event the same way.
  • 03
    Know what can mergeExplain how a namespace adds names to a function, and why two type aliases with one name are an error.

Two interfaces, one type

An interface gives a name to an object type: interface User { name: string; } says every User has a name that is text. In JavaScript, declaring the same name twice is usually a mistake. For interfaces, TypeScript allows it on purpose, and the two declarations join. You saw this briefly in Type aliases & interfaces; this lesson shows how it works and where it's useful:

User, declared twicePop out in the code editor (opens in a new tab)TypeScript
interface User {  name: string;}interface User {  email: string;} const asha: User = { name: "Asha", email: "asha@example.com" };console.log(asha.email);//               ^?

Lines 1 to 3 declare User with a name. Lines 4 to 6 declare User again, with an email. There is no error: TypeScript joins the two into one User that has both. Line 8 makes asha, a User with a name and an email, and line 9 prints asha@example.com. The query on line 10 asks what email is, and TypeScript answers (property) User.email: string: the email from the second declaration belongs to the one User.

Because there is only one User, an object needs every property from both declarations:

A User without an email (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
interface User {  name: string;}interface User {  email: string;} const sam: User = { name: "Sam" };

Line 8 makes sam with only a name. TypeScript marks line 8, on purpose here: Property 'email' is missing in type '{ name: string; }' but required in type 'User'. In plain words: the object you wrote has a name and nothing else, but a User must also have an email. Press Edit & run and add email: "sam@example.com" to line 8 to watch the error go away.

Two cards, each labelled interface User. One holds name: string and the other email: string. Arrows from both lead into one window titled User that holds both name: string and email: string.interface Username: stringinterface Useremail: stringUsername: stringemail: stringone type
Lines 1 to 6 declare User twice. TypeScript joins them into one User with a name and an email, so asha on line 8 needs both.

Joining declarations with the same name is called declaration merging. It sounds like a curiosity, but it's how you add your own properties to types you didn't write, like the browser's window.

Real-life analogyA binder that anyone can add pages to

An office keeps a binder for each customer, with the customer's name on the spine. Anyone who learns something new adds a page. When you open the binder, you read all of its pages together, as one record.

In real life: A binder with "User" on the spine
In TypeScript: The interface name, User
In real life: One person adds a page about the name
In TypeScript: interface User { name: string; }
In real life: Someone else adds a page about the email
In TypeScript: interface User { email: string; }
In real life: You read every page in the binder together
In TypeScript: TypeScript uses every property at once
In real life: Two pages can't disagree about the same item
In TypeScript: A property named twice must have the same type
The main idea

Declaration merging means TypeScript joins declarations that share a name into one. Two interfaces named User become one User with the properties of both. Two interfaces or two namespaces can merge; two type aliases or two classes can't.

How two interfaces join

Merging follows three rules:

  • Every property from every declaration goes into the one type. Three or more declarations work too.
  • A property can appear in more than one declaration, but only with the same type each time.
  • The declarations must be in the same place: in one file, or all in the global scope that every file shares. The next section shows the global scope.

Try the first two rules on a settings object. Lines 1 to 3 declare Settings with a theme. Each button puts something different in the second declaration, on line 5. Line 8 makes an object with a theme and a font size.

Playground: what the second Settings adds
Two Settings interfacesPop out in the code editor (opens in a new tab)TypeScript
interface Settings {  theme: string;}interface Settings {  fontSize: number;} const settings: Settings = { theme: "dark", fontSize: 16 };console.log(settings.theme, settings.fontSize);
Console outputdark 16
What happens
Line 5fontSize: number;
TypeScriptNo errors
Try it yourself

Line 5 adds fontSize. Settings now has a theme from line 2 and a fontSize from line 5, so the object on line 8 fits, and the code prints dark 16.

TypeScript checks the code for real each time. Each button changes only line 5. Reset goes back to Add fontSize.

Press each button and watch the list under the code. Add fontSize has no errors: the merged Settings has both properties. fontSize as text moves the error to line 8, because the object gives fontSize a number. Repeat theme names theme again with the same type, which is allowed. theme as a number gets Subsequent property declarations must have the same type on line 5: the later declaration disagrees with line 2. The console line never changes, because types are removed before the code runs. You can also press Edit & run and type your own line 5.

Adding your own property to Window

In the browser, window is the global object: it holds the page's global names (the var and the global object lesson covers it). Apps often put their own things on it, like feature flags the server sends with the page. In plain JavaScript you just set a new property. In TypeScript:

A new property on window (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
window.featureFlags = { newCheckout: true };console.log(window.featureFlags.newCheckout);

Line 1 sets window.featureFlags and line 2 reads it. TypeScript marks both lines, on purpose here: Property 'featureFlags' does not exist on type 'Window & typeof globalThis'. TypeScript comes with the browser's types, in a file called lib.dom.d.ts. There, the type of window is everything the Window interface lists, joined with & to every global name (typeof globalThis), an intersection like the ones in the previous lesson. featureFlags is in neither. The code still runs and prints true; TypeScript just hasn't been told about the property.

Window is an interface, so you can declare it again and your declaration joins the browser's:

Your Window joins the browser's WindowPop out in the code editor (opens in a new tab)TypeScript
declare global {  interface Window {    featureFlags: { newCheckout: boolean };  }} window.featureFlags = { newCheckout: true };console.log(window.featureFlags.newCheckout);//                 ^?

Lines 1 to 5 are a declare global block. declare means "this only describes something; it makes no code". global means the declarations inside go into the global scope, where the browser's Window lives. Lines 2 to 4 declare interface Window with one property, featureFlags, and it merges with the browser's Window. Line 7 sets the flags, and line 8 prints true with no errors. The query on line 9 shows (property) Window.featureFlags: { newCheckout: boolean; }.

A window titled lib.dom, the browser's types, holds innerWidth: number. A window titled app.ts, your file, holds featureFlags. Arrows from both lead into one window titled Window that holds innerWidth and featureFlags.lib.dominnerWidth: numberthe browser's typesapp.tsfeatureFlags: {…}your fileWindowinnerWidth: numberfeatureFlags: {…}one type for window
The browser's types declare interface Window with properties like innerWidth. Your declare global block declares it again, and the two join, so window.featureFlags is allowed.

Why the declare global wrapper? In a project, a file with import or export is a module, and most files are (Why modules covers them). This site checks every example as a module too, even without those lines. Names declared in a module stay in that module. Leave the wrapper out and see:

interface Window without declare global (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
interface Window {  featureFlags: { newCheckout: boolean };} window.featureFlags = { newCheckout: true };

Lines 1 to 3 make a new Window that only this file can see. The browser's Window is unchanged, so line 5 gets the same error as before: Property 'featureFlags' does not exist. In a project file with no import or export, declarations are global already, so interface Window { … } alone is enough there. You'll often see that in a file named like global.d.ts; a .d.ts file holds only types.

Merging a namespace into a function

In JavaScript, a function is an object, so it can have properties of its own (the Function objects lesson shows how). TypeScript's older way to describe that is a namespace. A namespace is a named group of values, written namespace Shop { … }. Names inside it marked export can be read from outside, like Shop.currency. Namespaces come from before JavaScript had modules. Today modules do their job, but you'll still meet namespaces in libraries' type files.

Namespaces with the same name merge, like interfaces:

Two Shop namespaces joinTypeScript
namespace Shop {  export const currency = "$";}namespace Shop {  export function label(price: number) {    return currency + price.toFixed(2);  }} console.log(Shop.label(3));

Lines 1 to 3 make a Shop namespace with an exported currency. Lines 4 to 8 declare Shop again and add a label function. The two join, so line 6 can use currency from the first block, and line 10 prints $3.00. Only exported names join: without export on line 2, line 6 would get Cannot find name 'currency'.

These namespace examples have no Edit & run. A namespace with values is one of the few TypeScript features that turns into JavaScript code instead of being removed, and this site's editor only removes types. Node.js's built-in TypeScript support refuses them for the same reason. They're still checked here, and you'll see the JavaScript the compiler writes below.

A namespace can also merge with a function of the same name. The namespace's exported names become the function's properties:

formatPrice is a function and a namespaceTypeScript
function formatPrice(price: number) {  return formatPrice.currency + price.toFixed(2);}namespace formatPrice {  export const currency = "$";} console.log(formatPrice(3));//          ^?console.log(formatPrice.currency);

Lines 1 to 3 declare a function formatPrice that reads formatPrice.currency. Lines 4 to 6 declare a namespace with the same name and an exported currency. The two merge, so line 2 is allowed. Line 8 prints $3.00. The query on line 9 gets a two-line answer, function formatPrice(price: number): string and namespace formatPrice: one name, two things. Line 10 reads the property and prints $.

A shaded function card, formatPrice(price), and a card from namespace formatPrice, currency = dollar sign. Arrows from both lead into a window titled one name. In it, formatPrice(3) gives $3.00 and formatPrice.currency gives the dollar sign.functionformatPrice(price)namespacecurrency = "$"one nameformatPrice(3)$3.00formatPrice.currency"$"
The function and the namespace share the name formatPrice. You can call it, like line 8, and read the namespace's currency from it, like line 10.

Two rules for this merge:

  • The namespace comes after the function. Put it first and TypeScript says A namespace declaration cannot be located prior to a class or function with which it is merged.
  • Both are in the same file, and only the namespace's exported names become properties.

Here is the JavaScript the TypeScript compiler writes for the example, which you can run:

What the compiler writes for formatPricePop out in the code editor (opens in a new tab)JavaScript
function formatPrice(price) {
  return formatPrice.currency + price.toFixed(2);
}
(function (formatPrice) {
  formatPrice.currency = "$";
})(formatPrice || (formatPrice = {}));
console.log(formatPrice(3));
console.log(formatPrice.currency);

Lines 1 to 3 are the function, with its types removed. Lines 4 to 6 are the namespace: a function that runs right away and sets formatPrice.currency to "$". The function is an object, so that just adds a property. Line 7 prints $3.00, and line 8 prints $.

In your own code you rarely need a namespace for this. TypeScript understands a property you set on a function in the same place you declare it:

A property set on a functionPop out in the code editor (opens in a new tab)TypeScript
function formatPrice(price: number) {  return formatPrice.currency + price.toFixed(2);}formatPrice.currency = "$"; console.log(formatPrice(3));

Line 4 sets formatPrice.currency, and TypeScript adds currency to the function's type, so line 2 has no error. This is plain JavaScript once the types are removed, so it runs anywhere. Line 6 prints $3.00.

Why type aliases don't merge

A type alias, written type Settings = { … }, gives a name to one type, finished when you write it. An interface is open: more declarations can add to it. You met this difference in Type aliases & interfaces. Try both with the same two property lists:

Playground: interface or type?
Settings, declared twicePop out in the code editor (opens in a new tab)TypeScript
interface Settings {  theme: string;}interface Settings {  fontSize: number;} const settings: Settings = { theme: "dark", fontSize: 16 };console.log(settings.theme, settings.fontSize);
Console outputdark 16
What happens
Declared withinterface
TypeScriptNo errors
Try it yourself

Two interfaces named Settings join into one type with a theme and a fontSize. The object on line 8 fits, and the code prints dark 16.

TypeScript checks the code for real each time. The buttons change lines 1 to 6. Reset goes back to Two interfaces.

Two interfaces join, with no errors. Two type aliases get Duplicate identifier 'Settings' on lines 1 and 4. "Identifier" means a name, so this says the name Settings is used twice. TypeScript then uses only the first Settings, which has no font size, so lines 8 and 9 get errors too. Two names joined with & is the type alias way to build on a type: give the first part its own name, BaseSettings, and make Settings from it with & (the Intersection types lesson covers &).

Left, two interfaces named Settings, one with theme and one with fontSize, join into one card with theme and fontSize: one type. Right, two type aliases named Settings with the same properties stay apart, with the error Duplicate identifier.two interfacesSettingsthemeSettingsfontSizetheme, fontSizeone typetwo type aliasesSettingsthemeSettingsfontSizeDuplicate identifieran error on both
Two interfaces named Settings join into one type. Two type aliases named Settings don't: TypeScript marks both with Duplicate identifier.

The table below compares the two. Read each row as a question, then the answer for an interface and for a type alias.

interface and type alias, side by side
Questioninterfacetype alias
Can you declare the same name twice?Yes. The declarations join into one type.No. TypeScript reports Duplicate identifier.
Can you add a property to the browser's Window?Yes. Window is an interface, so yours joins it.No. A type alias can only make a new name.
How do you build a bigger type from it?Declare it again, or write interface B extends A.Give the bigger type a new name: type B = A & { … }.
Can it name a union, like "light" | "dark"?No. An interface always describes an object.Yes. A type alias can name any type.

So when a library wants you to add to one of its types, it uses an interface. When you want a type nobody can add to by accident, a type alias does that.

Typing a custom cart event

A shop page can tell its other parts that the cart changed by sending an event on window. A CustomEvent carries extra data in its detail property (the Custom events lesson covers them). Here is a listener and the event, with no types added:

A cart event TypeScript doesn't know (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
window.addEventListener("cart:updated", (event) => {  console.log(`Cart has ${event.detail.count} items`);}); window.dispatchEvent(new CustomEvent("cart:updated", { detail: { count: 3 } }));

Line 1 listens for "cart:updated". Line 2 reads event.detail.count. Line 5 sends the event with a count of 3. The code runs and prints Cart has 3 items, but TypeScript marks line 2, on purpose here: Property 'detail' does not exist on type 'Event'. TypeScript doesn't know this event name, so it treats the event as a plain Event, which has no detail.

How does addEventListener know what an event is? The browser's types have an interface, WindowEventMap, that lists event names and their event types. For "click" it says a PointerEvent. WindowEventMap is an interface, so you can merge your own event into it:

Your event joins WindowEventMapPop out in the code editor (opens in a new tab)TypeScript
declare global {  interface WindowEventMap {    "cart:updated": CustomEvent<{ count: number }>;  }} window.addEventListener("cart:updated", (event) => {//                                       ^?  console.log(`Cart has ${event.detail.count} items`);}); window.dispatchEvent(new CustomEvent("cart:updated", { detail: { count: 3 } }));

Lines 1 to 5 add "cart:updated" to WindowEventMap. Its type, CustomEvent<{ count: number }>, means a custom event whose detail is { count: number }; the angle brackets fill in the type of detail (the Generic functions lesson explains them). Line 7 listens, and the query on line 8 shows the event is now that CustomEvent. Line 9 reads event.detail.count with no error, and the code prints Cart has 3 items.

This is the pattern behind "libraries let you add to their types": the library ships an interface, and your declaration joins it. Now sort some declarations by what TypeScript says about them:

Error or fine?
  • interface Tea { name: string } interface Tea { price: number }
  • interface Tea { price: number } interface Tea { price: number }
  • interface Tea { price: string } interface Tea { price: number }
  • type Tea = { name: string }; type Tea = { price: number };
  • type Tea = { name: string }; interface Tea { price: number }
  • class Cart {} class Cart {}
  • function formatPrice() {} namespace formatPrice { export const currency = "$"; }
  • namespace formatPrice { export const currency = "$"; } function formatPrice() {}
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

Merging only changes types. Here is a Window property that is declared but never set:

A property that is declared but never setPop out in the code editor (opens in a new tab)TypeScript
declare global {  interface Window {    featureFlags: { newCheckout: boolean };  }} console.log(window.featureFlags);

Lines 1 to 5 tell TypeScript that window.featureFlags exists, so line 7 has no error. But nothing ever sets it, and the types are removed before the code runs, so line 7 prints undefined. A declaration is a promise you still have to keep.

  • "The second interface replaces the first." No. Every declaration's properties are kept, and the one type has all of them.
  • "When two declarations disagree, the later one wins." No. A property named twice must have the same type each time. Otherwise the later one gets Subsequent property declarations must have the same type.
  • "declare global puts the property on window." No. It only describes the property. Your code still has to set it, or it's undefined.
  • "interface Window in any file adds to the browser's Window." No. In a module, it makes a new Window for that file. Wrap it in declare global.
  • "Type aliases merge too." No. A second type alias with the same name is a Duplicate identifier error. Use an interface, or a new name and &.
  • "Merging changes the JavaScript that runs." No. Interfaces are removed completely. Only namespaces with values turn into code.

The table below lists what can merge. Read each row as: this pair of declarations with one name, does it merge, and what TypeScript does.

What merges, and what doesn't
One name, declared asDoes it merge?What TypeScript does
Two interfacesYes.They join into one type with every property from both.
Two interfaces with one property of different typesNo.An error on the later one: Subsequent property declarations must have the same type.
Two namespacesYes.Their exported names join into one namespace.
A function, then a namespaceYes.The function gets the namespace's exported names as properties.
A namespace, then a functionNo.An error: the namespace must come after the function.
Two type aliasesNo.An error on both: Duplicate identifier.
A type alias and an interfaceNo.An error on both: Duplicate identifier.
Two classesNo.An error on both: Duplicate identifier.

Practice exercises

Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.

Exercise 1 · Warm-upCount what the merged type asks for

Product is declared three times. How many properties must every Product have?

Starter codePop out in the code editor (opens in a new tab)TypeScript
interface Product {  name: string;}interface Product {  price: number;}interface Product {  inStock: boolean;}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line TypeScript marks

    Two declarations of CartItem, one with a mistake. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    interface CartItem {
      name: string;
      price: number;
    }
    interface CartItem {
      price: string;
      quantity: number;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeTeach TypeScript about window.cartCount

      Make both errors go away without changing lines 1 and 2. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      window.cartCount = 3;console.log(window.cartCount);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeMake the two Settings declarations merge

        TypeScript marks lines 1, 4, and 8. Change the declarations so they merge. Which keyword did you use?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Settings = {  theme: string;};type Settings = {  fontSize: number;}; const settings: Settings = { theme: "dark", fontSize: 16 };

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeType a wishlist event

          A wishlist button sends an event. Make the error on line 2 go away by adding the event to the browser's types. What does the code print?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          window.addEventListener("wishlist:added", (event) => {  console.log(`Added ${event.detail.name}`);}); window.dispatchEvent(new CustomEvent("wishlist:added", { detail: { name: "Tea" } }));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeGive formatPrice a currency

            TypeScript marks line 2, and the code prints undefined4.00. Give formatPrice a currency of "$". What does the fixed code print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            function formatPrice(price: number): string {  return formatPrice.currency + price.toFixed(2);} console.log(formatPrice(4));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask which declarations share a name, and whether that kind of declaration can merge. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The declaration merging quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What happens when a file declares two interfaces with the same name?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line does TypeScript flag in these two Order interfaces?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                interface Order {
                  id: number;
                  paid: boolean;
                }
                interface Order {
                  id: string;
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7Does the object on line 8 fit the merged Profile?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                interface Profile {
                  name: string;
                }
                interface Profile {
                  email: string;
                }
                
                const asha: Profile = { name: "Asha" };

                Choose an answer to see the explanation.

              4. Question 4 of 7Why does window.cartCount = 3 get an error in a TypeScript file?

                Choose an answer to see the explanation.

              5. Question 5 of 7What does TypeScript say about these two type aliases named Theme?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Theme = { color: string };
                type Theme = { font: string };

                Choose an answer to see the explanation.

              6. Question 6 of 7What does this code print when it runs in a browser?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                declare global {
                  interface Window {
                    cartCount: number;
                  }
                }
                
                console.log(window.cartCount);

                Choose an answer to see the explanation.

              7. Question 7 of 7Where must a namespace go to merge with a function of the same name?

                Choose an answer to see the explanation.

              Key takeaways

              • Interfaces with the same name join into one type with every property from each declaration.
              • A property named in two declarations must have the same type, or the later one gets an error.
              • To add to a global type like Window or WindowEventMap from a module, declare it again inside declare global.
              • A declaration only describes. Your code still has to set the property it promises.
              • A namespace after a function with the same name gives the function properties. Namespaces with values become real code.
              • Two type aliases or two classes with one name don't merge: TypeScript marks both with Duplicate identifier.

              Remember the one-liner.
              Declare an interface again, and TypeScript adds your properties to the same type.

              Coming next: Classes in TypeScript. It types a class's fields, constructor, and methods, and shows the parameter-property shortcut.

              CompleteFrontend Clear concepts. Working examples.