Interface Declaration Merging in TypeScript

Alfred M. Adjei

In our previous post on TypeScript Mixins, we talked briefly about declaration merging in TypeScript. In these series of posts, we are going to go a little deeper starting with interfaces with interface declaration merging.

What is Declaration Merging?

Declaration merging is when the TypeScript complier merges two or more types into one declaration provided they have the same name.

TypeScript allows merging between multiple types such as interface with interface, enum with enum, namespace with namespace, etc. One notable merge that isnโ€™t permitted is class with class merging. If you want a functionality like that, checkout mixins.

Letโ€™s get started with interface with interface merging by looking at an example:

interface Person {
  name: string;
}

interface Person {
  age: number;
}

interface Person {
  height: number;
}

class Employee implements Person {
  name = "Mensah"
  age = 100;
  height = 40
}

const employee = new Employee();
console.log(employee) // {name: "Mensah", age: 100, height: 40}

Since all the interfaces were declared with the same name, Person, they are merged into one definition so the Employee class contains the properties from all the interfaces.

๐ŸŠ Alligator.io recommends

Our recommended TypeScript courses

Same property names in interfaces (non-functions)

If any of the interfaces to be merged contain the same property name and that property isnโ€™t a function, then the type of the properties must be the same or else the complier will throw an error.

interface Person {
  name: string;
  zipCode: string;
}

interface Person {
  age: number;
  zipCode: string; // acceptable
}

interface Person {
   zipCode: number; // error
}

Same property names in interfaces (functions)

When the elements in the merged interfaces are functions and they have the same name, they are overloaded, that is, depending on the type of argument passed, the appropriate function will be called.

interface Person {
  speak(words: string);
}

interface Person {
  speak(words: number);
}

const person: Person = {
  speak: (wordsOrNum) => wordsOrNum
}

console.log(person.speak("Hi")) // speak(words: string) is used
console.log(person.speak(2)) // speak(words: number) is used

When interfaces containing same-signature functions are merged, the functions in the last declared interfaces appear at the top of the merged interface and the functions declared in the first interface appear beneath.

interface Person {
  speak(words:string);
}

interface Person {
  speak(words: any);
}

interface Person {
  speak(words: number);
  speak(words: boolean);
}

// This is how the final merged interface looks like
interface Person {
  // functions in the last interface appear at the top
  speak(words: number);
  speak(words: boolean);

  // function in the middle interface appears next
  speak(words: any):number;

  // function in the first interface appears last
  speak(words: string):string;
}

The reason for this is, is that later interfaces declared have a higher precedence over the initial interfaces declared. So in our example above, speak(words: string) will never be called because in the final merged Person interface, speak(words: any):number comes before speak(words: string):string and since any can stand for any type, it gets called even if a string is passed as an argument.

To prove this, when you hover over the per variable in the code below, it will display const per: number and not const per: string even though we are passing in a string argument.

const per = person.speak("bacon");

This is true for all interfaces expect when the same name functions parameters have a string literal as a type. It follows the same order described above but functions with string literal types are given a higher precedence and therefore appear at the top.

Itโ€™s worth noting that, the string literals in later interfaces will appear before the initial interfaces like explained above.

interface Person {
  speak(words: number);
  speak(words: "World!"); // string literal type
}

interface Person {
  speak(words: "Hello"); // string literal type
}

interface Person {
  speak(words: string);
}


// merged interface output.
interface Person {
  // string literals are given precedence
  speak(words: "Hello");
  speak(words: "World!");

  // the rest of the functions are arranged using the same rules.
  speak(words: string);
  speak(words: number);
}

Thatโ€™s it. Hope this was useful. ๐Ÿ˜Š๐Ÿ˜Ž

  Tweet It

๐Ÿ•ต Search Results

๐Ÿ”Ž Searching...

Sponsored by #native_company# โ€” Learn More
#native_title# #native_desc#
#native_cta#