DEV Community

Cover image for The Difference Between Types (Type Aliases) And Interfaces in TypeScript
Andrew Savetchuk
Andrew Savetchuk

Posted on • Updated on • Originally published at blog.savetchuk.com

The Difference Between Types (Type Aliases) And Interfaces in TypeScript

Today, more and more people and companies are using TypeScript. It is among the top 10 programming languages in the PYPL index. And this is not surprising, since TypeScript allows you to catch many errors at compile time, not at runtime, that is before they are released to production.

Top 10 programming languages according to PYPL Index as of February 2023

Top 10 programming languages according to PYPL Index as of February 2023.

Some of you who decide to try TypeScript might get confused with type aliases and interfaces because they are both similar and used to define the shape and structure of values in a statically typed way. This means that it is possible to describe the same object using a type alias or an interface:

// Option #1: using a type alias
type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

// Option #2: using an interface
interface Person {
  firstName: string;
  lastName: string,
  age: number;
};
Enter fullscreen mode Exit fullscreen mode

You might be asking yourself why we have two options for describing the Person object instead of one. And are there any differences or specific use cases between type aliases and interfaces? Yes, and you will find out about them in a moment.

Before we begin, I should mention that the correct term for a type is type alias, but for simplicity, I will use those two terms interchangeably.

Difference #1: Objects and Functions

Both types and interfaces can be used to describe the shape of an object or a function signature. However, the syntax is different. A type is defined using the type keyword, followed by the name, an equals sign and the type definition:

type SampleObject = {
  a: string;
  b: string;
};
Enter fullscreen mode Exit fullscreen mode

An interface is defined using the interface keyword, followed by the name and the interface definition in curly braces:

interface SampleObject {
  a: string;
  b: string;
};
Enter fullscreen mode Exit fullscreen mode

As you can see, there is not much difference in syntax between the two examples. However, when we want to define a function signature, the difference becomes more noticeable:

// Using the "type" keyword
type SampleFunction = (param1: string, param2: number) => boolean;

// Using the "interface" keyword
interface SampleFunction {
  (param1: string, param2: number): boolean;
}
Enter fullscreen mode Exit fullscreen mode

In practice, either approach can be used to define a function in TypeScript, and the choice between them often comes down to personal preference or project standards.

Difference #2: Other Types

Unlike an interface, the type alias can be used to create named types for all other types including:

  • primitives

  • unions (union types)

  • tuples

  • mapped types

Primitives

Just in case, I will remind you that there are six primitive types in JavaScript and TypeScript: boolean, number, string, null, undefined, and symbol. Here is an example of how to represent primitives in TypeScript using the type keyword:

type Name = string;
type Age = number;
type IsStudent = boolean;
Enter fullscreen mode Exit fullscreen mode

With interfaces, you cannot directly define a named type for a primitive type.

Unions

A variable with a union type can hold a value of any of the constituent types. Union types can be formed by combining two or more other types, separated by a vertical bar (|).

// Example #1: union type that allows a variable to hold 
// one of the three specified string literal values.
type SampleType1 = 'Coffee' | 'Tea' | 'Water';

// Example #2: union type that allows a variable to hold 
// a value that is either a string or a number.
type SampleType2 = string | number;

// Example #3: union type that allows a variable to hold
// any of the values that are allowed by SampleType1 or SampleType2
type SampleType = SampleType1 | SampleType2;
Enter fullscreen mode Exit fullscreen mode

Same as with primitives, you cannot directly define a union type using the interface keyword, however, it is possible to have a union type as an object property:

interface SampleObject {
  myProperty: string | number;
}
Enter fullscreen mode Exit fullscreen mode

There are some limitations when using union types that you should be aware of:

  • An interface cannot extend a union type:
type SampleUnionType = string | number;

// The code below will not work
interface extends SampleUnionType { ... }
Enter fullscreen mode Exit fullscreen mode
  • A class cannot implement a union type:
type SampleUnionType = string | number;

// The code below will not work
class SampleClass implements SampleUnionType { ... }
Enter fullscreen mode Exit fullscreen mode

A class can implement an interface or type alias, both in the same way. However, it can not implement a type alias that names a union type.

Tuples

A tuple is a type that allows you to specify a fixed-length array of values where each element has a specific data type. Here is an example of how to create a type for an array with only two elements:

type PersonTuple = [string, number];

const person: PersonTuple = ["John Doe", 25];
Enter fullscreen mode Exit fullscreen mode

Even though this can be achieved using interfaces as shown below, you will lose access to all of the array methods. If you try to call person.length on the variable created from the interface, you will get the following error: Property 'length' does not exist on type 'PersonTuple'.

You will have to manually add those methods (length, push, concat, etc.) to the interface as shown below. However, this is not something you want to do. If you need a tuple, consider using a type alias.

// How to mimic array methods
interface PersonTuple
{
    0: string;
    1: number;
    length: 2;
}

const person: PersonTuple = ["John Doe", 25];

console.log(person.length); // This will now work
Enter fullscreen mode Exit fullscreen mode

Mapped Types

Mapped types allow you to transform an existing type into a new type by applying a mapping operation to each property of the original type.

type NewType = {
  [Property in OldType]: MappingOperation;
}
Enter fullscreen mode Exit fullscreen mode

Here, OldType is the original type that you want to transform, and Property represents each property in OldType. MappingOperation is a TypeScript type operator that defines how each property in OldType will be transformed.

In the example below we create a new type B, which has all the properties of A, but with each property made optional.

type B = {
  [Property in keyof A]?: A[Property];
}
Enter fullscreen mode Exit fullscreen mode

TypeScript does not currently support typing mapped types with interfaces.

Difference #3: Declaration Merging

If you have two interfaces that you define with the same name in the same scope, they will be merged. Consider this example:

// Declaring SampleType interface for the 1st time
interface SampleType {
  a: number;
  b: number;
};

// Declaring SampleType interface for the 2nd time
interface SampleType {
  c: number;
  d: number;
};

// The second interface declaration extends the first one by adding
// two new properties. Now SampleType has four properties: a, b, c, d
const sampleType: SampleType = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
};
Enter fullscreen mode Exit fullscreen mode

We cannot do the same with types. This is important to consider if you are the author of a library and want your users to be able to extend its functionality by merging interfaces. It will allow them to combine their own type declarations with those provided by the library, effectively "extending" or "augmenting" the library's API.

Difference #4: Extention

Interfaces can extend other interfaces using the extends keyword which allows for greater flexibility and modularity:

interface Named {
  name: string;
}

interface Ageable {
  age: number;
}

interface Person extends Named, Ageable {
  sayHello(): void;
}
Enter fullscreen mode Exit fullscreen mode

Types can also be extended using an intersection type. An intersection type combines multiple types into one. Consider this example:

// Example #1
type A = { a: number };
type B = { b: number };
type C = A & B;

const c: C = { a: 10, b: 20 };

// Example #2
type A = { a: number };
type B = A & { b: number };

const b: B = { a: 10, b: 20 };
Enter fullscreen mode Exit fullscreen mode

As you can see, the & operator is used to create a new type by combining multiple existing types. The new type has all properties of the existing types.


We can also mix them together as shown below. Interfaces and type aliases are not mutually exclusive. An interface can extend a type alias and vice versa.

  • Interface extends another interface:
interface A { sampleProperty: number; }
interface B extends A { sampleProperty: number; }
Enter fullscreen mode Exit fullscreen mode
  • Interface extends a type alias:
type A = { sampleProperty: number; }
interface B extends A { sampleProperty: number; }
Enter fullscreen mode Exit fullscreen mode
  • Type alias extends another type alias:
type A = { sampleProperty: number; };
type B = A & { sampleProperty: number; };
Enter fullscreen mode Exit fullscreen mode
  • Type alias extends interface:
interface A { sampleProperty: number; }
type B = A & { sampleProperty: number; };
Enter fullscreen mode Exit fullscreen mode

There are some limitations to what can be extended with interfaces. For example, primitive types, union types, classes, and enums, cannot be extended with interfaces.

What Should I Use?

In general, it is recommended to use interfaces for defining object shapes, and types for defining other types of values. However, it is up to you and your team to decide which option to use as long as you are consistent.

For example, every object should be typed as an interface and everything else as a type. Or you can use interfaces or types for everything, but not both for values of the same type (e.g. types and interfaces for objects). Communication with your team and consistency are crucial for quality code.

Here is a good cheat sheet that I often refer to, however, it may differ for everyone:

When to use type:

  • Use type when defining primitives

  • Use type when defining unions

  • Use type when defining tuples

  • Use type when defining functions

  • Use type when defining mapped types

When to use interface:

  • Use interface for all object types where using type is not required

  • Use interface when you want to take advantage of declaration merging

Keep an Eye on the Changelog

The limitations and differences described in this article are current as of February 2023. However, with new versions of TypeScript, they can change. It is important to keep an eye on the release notes to stay informed about new changes and features.


The end, I hope this information was helpful, stay tuned for more content :)


Source

  1. PYPL PopularitY of Programming Language

  2. Interfaces vs Types in TypeScript - Stack Overflow

  3. TypeScript: Documentation - Everyday Types

  4. TypeScript: Documentation - Mapped Types

Top comments (0)