DEV Community

Cover image for Types vs. Interfaces in Typescript
ananta
ananta

Posted on

Types vs. Interfaces in Typescript

Note: The term "Types" in this article is referring to the"type" aliases in Typescript

According to the official Typescript document: "TypeScript is an open-source language that builds on JavaScript, one of the world’s most used tools, by adding static type definitions." Implementing typescript on our project saves a lot of time in the long run. But as a developer, we should follow the best practices and standards when working with types in typescript.
In this article, we will take a look at the differences between two types type aliases and interface. Many developers don't really know the real difference between these two. Having known the differences, we can implement these according to the best use case for us.

Origin

Interfaces are used to define data structures, for example, an Object's shape/structure.

Types are used to define the type of data, for example: Primitive, Union, Intersection, Tuple data types.

Type Evaluation Time

There is a subtle key difference in the type evaluation period. Type evaluation on type aliases is immediate while Type evaluation on the interface is lazy.

Type Declaration Syntax

Even though both types and interfaces can be used in a similar way, their declaration syntax differs.

type BulldogType = {
  name: string;
  height: number;
  weight: number;
}

interface BulldogInterface {
  name: string;
  height: number;
  weight: number;
} 
Enter fullscreen mode Exit fullscreen mode

Extends & Implements keyword

In Typescript, we can extend and implement types using the interface. This is not possible using the type aliases.

interface Dog {
  breed: string;
}

interface Bulldog extends Dog {
  isCute: true;
}
Enter fullscreen mode Exit fullscreen mode

Intersection

We can combine multiple types and interface with the"&" keyword into a single type. But, we cannot combine them into a single interface.

type Bulldog = { }
type GermanShepherd = {}

type DogBreeds = Bulldog & GermanShepherd; // valid

interface IBulldog {}
interface IGermanShepherd {}

type IDogBreeds = IBulldog & IGermanShepherd; // valid

Enter fullscreen mode Exit fullscreen mode

Unions

Union types allow us to create a new type that can have a value of one or a few more other types with the"|" keyword.
We can combine multiple types and interface with the union keyword into a single type. But, we cannot combine them into a single interface.

type Bulldog = { }
type GermanShepherd = {}

type DogBreeds = Bulldog | GermanShepherd; // valid

interface IBulldog {}
interface IGermanShepherd {}

type IDogBreeds = IBulldog | IGermanShepherd; // valid
Enter fullscreen mode Exit fullscreen mode

Declaration merging

interface allows for declaration merging whilst type aliases don't. Typescript compiler intelligently merges two or more interfaces that share the same name into only one declaration.

interface IBulldog {
 name: string;
}
interface IBulldog {
 weight: number;
}

const bruno: IBulldog = {
  name: "Bruno",
  weight: 22
}
Enter fullscreen mode Exit fullscreen mode

But type aliases can't be changed once a type is created using the type alias. So, declaration merging isn't possible with the type aliases.

type IBulldog = {
 name: string;
}
type IBulldog = {
 weight: number;
}

// Error: Duplicate identifier 'IBulldog'
Enter fullscreen mode Exit fullscreen mode

Top comments (11)

Collapse
 
peerreynders profile image
peerreynders

An interface merge can be used to "add" new functionality to an existing class (as long as only public features are accessed):

class MyClass {
  readonly #value: number;

  constructor(value: number) {
    this.#value = value;
  }

  get value():number {
    return this.#value;
  }
}

interface MyClass {
  square():number;
}

function square(this:MyClass): number {
  const value = this.value;
  return value * value; 
}

MyClass.prototype.square = square;

const container = new MyClass(42);
console.assert(container.square() === 1764, "Square isn't 1764");
Enter fullscreen mode Exit fullscreen mode
Collapse
 
harisharaju1 profile image
Harish Raju

So what is the difference between combining two types or interfaces, using '&' and using '|'.

Collapse
 
peerreynders profile image
peerreynders

See Naming of TypeScript's union and intersection types:

type Dog = {
  breed: string;
};

// Intersection
type Bulldog = {
  isCute: boolean;
} & Dog;

const winston: Bulldog = {
  breed: 'Bulldog',
  isCute: false,
};

// Union
type Pet = {
  name: string
} | Dog;

const cat: Pet = {
  name: 'Fluffykins'
};

const fido: Pet = {
  name: 'froufrou',
  breed: 'Maltese'
};

const dog: Pet = {
  breed: 'Boston Terrier'
}
Enter fullscreen mode Exit fullscreen mode

intersection of types (&): the shape of the data has to conform to the constraints of all the intersected types simultaneously.

union of types (|): the shape of the data has to conform to the constraints of at least one of the unioned types.

Collapse
 
adamellsworth profile image
Adam

isCute: false, but Winston IS cute :(
(lol just being silly. Thank you for the further clarification/doc resrouce)

Collapse
 
harisharaju1 profile image
Harish Raju

thank you for explaining along with code snippets.....appreciate it!!

Collapse
 
codinghusi profile image
Gerrit Weiermann

The & will merge the types, so all the properties of both types will be in the resulting type.

The | will give you the option to choose between all the given types, and use one of them, rather than all together :)

Collapse
 
larsejaas profile image
Lars Ejaas

As a developer new to Typescript I struggle to see where I would need "type" really? 🤔
I am sure there are edge cases, but for now I just rely on "interface" .

Collapse
 
peerreynders profile image
peerreynders

Typically one is more comfortable with interface if one's zone of familiarity is in terms of class-oriented object orientation, i.e. one predominantly thinks of objects in terms of "instances of a class".

type is more useful when you are typing "general data". Types aren't limited to "objects as structured data" but also include literal types. The type syntax also has a lot of features to pull information from JavaScript's value space into TypeScript's type space.

const transform = {
  A: 'a',
  B: 'b',
  C: 'c',
  D: 'd',
  E: 'e',
} as const;

type Transform = typeof transform;
type Key = keyof Transform;      // type Key = "A" | "B" | "C" | "D" | "E"
type Value = Transform[Key];     // type Value = "a" | "b" | "c" | "d" | "e" 

function convert(original: Key): Value {
  return transform[original];
}

console.assert(convert('D') === 'd', 'Value mismatch');
Enter fullscreen mode Exit fullscreen mode

Above transform and convert exist in JavaScript's value space while Transform, Key and Value exist in TypeScripts's type space.

One of the more interesting types are discriminated unions:

const entries = {
  A: 1,
  B: 2,
  C: 3,
  D: 4,
  E: 5,
} as const;

type Entries = typeof entries;
type Key = keyof Entries;      // type Key = "A" | "B" | "C" | "D" | "E"
type Value = Entries[Key];     // type Value = 1 | 2 | 3 | 4 | 5
type Entry = [Key, Value]; 

const data = Object.entries(entries) as [[Key,Value]];
const reversed = new Map(data.map(([key, value]) => ([value, key])));

// Discriminated union
type FindByKey = {
  findBy: 'Key';
  key: Key;
}

type FindByValue = {
  findBy: 'Value';
  value: Value;
}

type FindBy = FindByKey | FindByValue;

function find(config: FindBy): Entry {
  switch(config.findBy) {
    case 'Key':
      // i.e. FindByKey here
      return [config.key, entries[config.key]];
    case 'Value':
      // i.e. FindByValue here
      return [reversed.get(config.value)!, config.value]; 
  }
}

console.assert(find({ findBy: 'Key', key: 'D'})[1] === 4, 'Wrong value');
console.assert(find({ findBy: 'Value', value: 4})[0] === 'D', 'Wrong key');
Enter fullscreen mode Exit fullscreen mode

So from that perspective type is my default - unless for some reason I'm working with classes then interface is a better fit.

Collapse
 
larsejaas profile image
Lars Ejaas

Wow appreciate your feedback. But, I think must of the stuff you described here is a bit beond my current skill level. I use typescript with React for now, and I have to admit I feel unsure why your above example wouldn't have worked equally well with "interface" instead of "types" 🤔

Thread Thread
 
peerreynders profile image
peerreynders

why your above example wouldn't have worked equally well with "interface" instead of "types" 🤔

Actually it's easier to explore when to use interface instead of type:

  • interface declarations merge, type aliases do not. So if it is necessary to declare an interface in bits-and-pieces interface is the only choice especially when monkey patching an existing class (which really should be avoided for built-in and even third party classes). With type each piece needs to be a separate type which are then combined by intersecting them.

  • By convention use interface not type when the declaration is going to be implemented by a class:

While syntactically correct

type Title = {
  title: string;
};

class Dog implements Title {
  #title: string;

  constructor(breed: string, name: string) {
    this.#title = `${name} (${breed})`;
  }

  get title(): string {
    return this.#title;
  }
}

const fido = new Dog('Maltese', 'Froufrou');
const expected = 'Froufrou (Maltese)';
console.assert(fido.title === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

a class should implement an interface, not a type:

interface Title {
  title: string;
};

class Dog implements Title {
  #title: string;

  constructor(breed: string, name: string) {
    this.#title = `${name} (${breed})`;
  }

  get title(): string {
    return this.#title;
  }
}

const fido = new Dog('Maltese', 'Froufrou');
const expected = 'Froufrou (Maltese)';
console.assert(fido.title === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

Everywhere else use type to get full access to TypeScript's typing features.

So for object types that are not class-based it makes sense to use type.

type Key = 'A' | 'B' | 'C' | 'D' | 'E'; // Can't do this with `interface` because `Key` isn't an object type
type Value = 1 | 2 | 3 | 4 | 5;
type Entry = [Key, Value];              // Again a tuple isn't an object type - so `interface` is no help here
                                        // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-3.html#tuple-types
Enter fullscreen mode Exit fullscreen mode

Derived object types become type aliases, not interfaces:

const fido = { 
  breed: 'Maltese', 
  name: 'Froufrou',
  title: () => `${fido.name} (${fido.breed})`
};

type Dog = typeof fido; // type Dog = { breed: string, name: string, title: () => string }

const expected = 'Froufrou (Maltese)';
console.assert(fido.title() === expected, `Title not "${expected}"`);
Enter fullscreen mode Exit fullscreen mode

type supports Mapped Types and Generics:

type KeyName = 'name' | 'breed'
type Dog<T> = {
  [key in KeyName]: T;
}

{
  const name = 'Froufrou';
  const breed = 'Maltese';
                     // type inference:
  const dog = {      // const dog: { name: string, breed: string }
    name,
    breed 
  };
                             // type inference:
  const fido:Dog<string> = { // const fido: Dog<string>
    name,
    breed 
  };

  console.assert(name === dog.name && breed === dog.breed, 'dog: No match')
  console.assert(name === fido.name && breed === fido.breed, 'fido: No match')
}
Enter fullscreen mode Exit fullscreen mode

Most of the Utility Types are defined via type aliases.

Because of the versatility of type aliases open source projects like comlink use them heavily - so being able to decipher type aliases can be helpful to understand the type constraints.

By limiting yourself to interface you aren't leveraging TypeScript's features as much as you could.

People usually get into type aliases once they realize how useful sum types (Union types) really are.

const left: unique symbol = Symbol('Left');
const right: unique symbol = Symbol('Right');

// Discriminating Union + Generics
type Either<L,R> = [typeof left, L] | [typeof right, R];

function showResult(result : Either<string,number>): void {
  switch(result[0]) {
    case left:
      console.log('Error (Left):', result[1]);
      break;
    case right:
      console.log('Success (Right):', result[1].toFixed(2));
      break;
  }
}

function validate(value: number): Either<string, number> {
  return value < 10 ? [right, value] : [left, 'Too Large'];
}

showResult(validate(Math.PI)); // 'Success (Right): 3.14'
showResult(validate(10));      // 'Error (Left): Too Large'
Enter fullscreen mode Exit fullscreen mode

Another nifty thing one can do with type:

// Only exists in the "type context"
declare const emailVerified: unique symbol;

// Make structurally different from plain `string`
type Email = string & {
  [emailVerified]: true
}

const verifiedEmails = new Set(['jane.doe@example.com']);

// Assertion function
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions
function assertIsEmail(email: string): asserts email is Email {
  if (!verifiedEmails.has(email)) throw new Error(`"${email}" is not a verified email`);
} 

// export this function
function validateEmail(email: string): Email {
  assertIsEmail(email);   // `email: string`
  return email;           // `email: Email`
}

// Simple type alias
type EmailAlias = string;

try {
  const email = 'jane.doe@example.com';
  const verified = validateEmail(email); // const verified: Email
  console.log('Verified:', verified);    // 'Verified: jane.doe@example.com'

  const another = 'john.doe@example.net';
  const unverified: EmailAlias = another; // Simple type alias **will not** cause an error; however
  // const unverified: Email = another;   // Type 'string' is not assignable to type 'Email'.
                                          // Type 'string' is not assignable to type '{ [emailVerified]: true; }'.(2322)
  const forced: Email = another as Email; // However type assertion can silence the error on `Email` and narrow the type.
                                          // https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
  console.log('Forced:', forced);         // 'Forced: john.doe@example.net'

  const notVerified = validateEmail(another);
  // Never gets here
  console.log('Unverified', unverified);

} catch(e) {
  console.log(e.message);                // "john.doe@example.net" is not a verified email
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript's type context Email is structurally different from string - even though in JavaScript's value context it simply is a string.

This can help prevent a regular string from being assigned to Email without being validated (though a type assertion can force it) - without having to resort to a holder object:

type Email = {
  email: string;
}

const verifiedEmails = new Set(['jane.doe@example.com']);

function validateEmail(email: string): Email {
  if (!verifiedEmails.has(email)) throw new Error(`"${email}" is not a verified email`);
  return {
    email
  };
}

try {
  const email = 'jane.doe@example.com';
  const verified = validateEmail(email);    // const verified: Email
  console.log('Verified:', verified.email); // 'Verified: jane.doe@example.com'

  const another = 'john.doe@example.net';
  const forged: Email = { email: another };
  console.log('Forged:', forged.email );    // 'Forged: john.doe@example.net'

  const notVerified = validateEmail(another);
  // never gets here

} catch(e) {
  console.log(e.message);                // "john.doe@example.net" is not a verified email
}
Enter fullscreen mode Exit fullscreen mode

So types go beyond classes and interfaces. If you're not using type what are you using TypeScript for?

Collapse
 
arnoldddev profile image
Arnold

Interface better