in

Demystifying Type Aliases vs Interfaces in TypeScript

As a TypeScript developer, understanding the nuances between type aliases and interfaces is key to writing clean, maintainable code. While they appear interchangeable at first glance, there are some important semantic and syntactic differences.

In this comprehensive 4-part guide, we’ll unpack when and why to use type aliases versus interfaces in TypeScript.

A TypeScript Refresher

For context, let‘s briefly recap what TypeScript is and why it can be helpful.

TypeScript is a superset of JavaScript – meaning it adds new features and capabilities on top of vanilla JS. Specifically, TypeScript introduces optional static type checking.

This means we can annotate variables and function parameters with types like:

const name: string = "John";

function greet(name: string) {
  return `Hello ${name}!`; 
}

The key benefit of TypeScript’s static typing is preventing an entire class of errors – type errors. By telling the compiler the expected types upfront, it can warn us if we ever try to assign the wrong type somewhere or pass invalid arguments to a function.

Catching these bugs during development saves debugging time down the line. That‘s why adding types with TypeScript has become so popular for large JavaScript codebases.

Okay, now that we‘ve refreshed on TypeScript, let‘s focus back on type aliases versus interfaces specifically…

Type Aliases

Type aliases allow us to create reusable "aliases" for existing types.

Here is the syntax for creating a type alias:

type AliasName = ExistingType;

For example, we could create an alias for a string literal type:

type Message = "success" | "error";

Or alias something complex like a union of multiple types:

type StringOrNumber = string | number;

We can also use type aliases to extract reusable object types:

type User = {
  name: string;
  id: number;
};

const user: User = {
  name: "John", 
  id: 1
}; 

The User alias serves as documentation for what properties a User object should contain. The compiler will enforce that any variables of type User match that shape.

Some key characteristics of type aliases:

  • Can extract reusable primitive, union, intersection, and object types
  • Cannot be extended or implemented from (more on this later)
  • Often serve as low-level building blocks in a codebase

Overall, type aliases shine for creating simple reusable types that need no extra behavior attached.

Interfaces

Interfaces are also used to extract reusable object types in TypeScript.

Here is how we define an interface:

interface InterfaceName {
  propertyName: Type; 
}

For example:

interface User {
  name: string;
  id: number;
}

const user: User = {
  name: "John",
  id: 1
};

So far, this User interface looks identical in usage to the type alias example above.

However, interfaces have some unique capabilities that type aliases lack:

  • Interfaces can be extended and merged together
  • Classes and objects can implement interfaces
  • Interfaces can describe function types in addition to object shapes

Let‘s look at each of those key features in more depth.

Extending Interfaces

A major difference between type aliases and interfaces is that interfaces can be extended using the extends keyword:

interface Base {
  propA: string;
} 

interface Extended extends Base {
  propB: string;
}

This allows us to build up interfaces incrementally. For example, we might start with a simple Board interface:

interface Board {
  width: number;
  height: number;
}

Then extend it to add additional requirements:

interface BoardWithPieces extends Board {
  pieces: Piece[];
}

Trying to extend type aliases like this isn’t possible:

type Base = {
  propA: string; 
};

// ERROR 
type Extended extends Base {
  propB: string;
}

With type aliases, we‘d have to fall back to intersections:

type Base = {
  propA: string;
};

type Extended = Base & {
  propB: string; 
};

Which is more verbose than simply extending the interface directly.

So extending interfaces is preferred when we need to build up types incrementally.

Merging Interfaces

Related to extends, another unique capability of interfaces is that they can be merged together when multiple interfaces with the same name are defined:

interface Merged {
  propA: string;
}

interface Merged {
  propB: number;
}

const example: Merged; // Contains propA and propB

This allows us to logically group related properties together into coherent interfaces even when defined across multiple files or declarations.

With type aliases, only a single declaration is allowed – additional declarations with the same name would be ignored.

Implementing Interfaces

Interfaces can be implemented by classes and objects using the implements keyword.

For example, we could define an interface for validating objects:

interface Validatable {
  isValid(): boolean; 
}

Then implement it on a class:

class User implements Validatable {
  // ...

  isValid() {
    return true; 
  }
}

This enforces that any Validatable object, like User, contains the methods defined on the interface.

We cannot implement or extend from type aliases in this way.

Overall, being able to implement interfaces is powerful for defining contracts or guarantees that other classes must satisfy.

Describing Function Types

In addition to describing object shapes, interfaces can also describe function signatures.

For example:

interface SearchFunction {
  (source: string, subString: string): boolean;
}

This defines the call signature for any functions assignable to SearchFunction.

We could then implement that interface on a function:

const mySearch: SearchFunction = (src, sub) => {
  return src.search(sub) !== -1;
} 

And the compiler will check for matching parameters and return type.

Describing function types is possible with type aliases as well, but less flexible than interfaces in some cases.

Type Alias vs Interface Usage Guidelines

Given the various overlaps and differences, when should you use type aliases versus interfaces in practice?

Here are some guidelines I follow:

  • Type aliases for reusing primitive types like unions, tuples, and literals
  • Type aliases for aliased object types that extend other object types
  • Interfaces for object types, especially those that need to be extended
  • Interfaces for options objects used by public APIs
  • Interfaces over type aliases when possible for consistency

Beyond this basic guidance, it depends on your codebase conventions and personal preference. The important thing is to pick one style and stick with it consistently.

Let‘s walk through a few concrete examples to solidify when to use each one.

Imagine we are building a React application for displaying user profiles. We need to define types for:

  • The shape of a user object returned from our API
  • Different types of profile picture sizes

For the User object, we‘ll want to enforce the shape but also potentially extend it later. Using an interface makes the most sense:

interface User {
  id: string;
  name: string;  
  profilePicture: string;
}

For the photo sizes, we just want a reusable set of string literals – type alias fits the bill:

type PhotoSize = ‘small‘ | ‘medium‘ | ‘large‘;

We may also have a utility function for formatting the user‘s full name:

interface NameFormatter {
  (user: User): string; 
}

const formatter: NameFormatter = (user) => {
  return `${user.name} (${user.id})`; 
};

Using interface over type alias for the function signature keeps things consistent.

Adopting conventions like this helps keep unnecessary cognitive overhead at a minimum.

Key Takeaways

Let‘s recap the key differences between type aliases and interfaces:

  • Type aliases are great for composable, reusable types like unions, tuples, and primitives
  • Interfaces can be extended and merged together
  • Interfaces can be implemented by classes
  • Interfaces are more flexible for describing function types

Some general guidelines:

  • Use type aliases for simple composable types
  • Use interfaces for object shapes and public API contracts
  • Prefer interfaces over aliases when in doubt

Learning how to leverage both tools effectively takes time. But doing so will help you write cleaner, more maintainable TypeScript code.

Understanding these nuances transforms how you approach structuring your types. So be patient in learning them, and happy type aliasing and interfacing!

AlexisKestler

Written by Alexis Kestler

A female web designer and programmer - Now is a 36-year IT professional with over 15 years of experience living in NorCal. I enjoy keeping my feet wet in the world of technology through reading, working, and researching topics that pique my interest.