Demystifying TypeScript Types vs Interfaces

As a developer, few technologies have skyrocketed in popularity over the last few years like TypeScript. Adoption of TypeScript has increased 5x since 2016, with OVER 50% OF DEVELOPERS now utilizing it in projects. What started out as a small open-source project from Microsoft has become a mainstay of modern JavaScript engineering.

But with great scale comes great responsibility – well typed responsibility that is! Mastering the TypeScript type system is integral to leveraging it effectively. This means understanding the core difference between two pivotal concepts – type aliases declared with the type keyword, and interfaces declared using the interface keyword.

In this comprehensive guide as we dive deep into all aspects, we will answer questions like:

  • What situations call for using types vs interfaces?
  • What are some key capabilities of each?
  • How do we leverage them effectively together?

Let‘s unravel the mystery step-by-step…

TypeScript Typing Basics

First, a quick primer on TypeScript. At a high level, TypeScript provides optional static typing for JavaScript applications. This means:

  • We can choose how much we want to use types
  • Types are used at compile time rather than runtime
  • TypeScript gets compiled down to regular JavaScript

Adding proper types provides all sorts of benefits:

// With Types 
function sum(a: number, b: number) {
  return a + b;
} 

sum(10, 20); // ok
sum(‘10‘, 20); // Compile Error  

// Without Types
function sum(a, b) {
  return a + b; 
}

sum(10, 20); // ok  
sum(‘10‘, 20); // Runtime Exception

As we can see, types help catch a whole class of errors during compilation rather than at runtime. This helps boost productivity and prevents users from encountering unintended issues.

In the example above, we used some of TypeScript‘s built-in basic types like number and string. But to truly harness typing, we need to be able to define custom object shapes and reuse types. This leads us to type aliases and interfaces

Type and Interface Syntax

The syntax for types and interfaces is straightforward. Here is the same object shape defined using each:

With Type

type Site = {
  name: string;
  url: string;
  pageCount: number;   
};

With Interface

interface Site {
  name: string;
  url: string;  
  pageCount: number;
};

Then we can reuse the Site shape to annotate other code:

function printSite(site: Site) {
  console.log(site)  
}

const mySite: Site = {
  //...
};

So in basic usage, both types and interfaces allow defining reusable object types to enable powerful type checking.

However, there are some very notable differences we will uncover as we examine capabilities more deeply.

Key Differences

Understanding the key differences between type aliases and interfaces unlocks being able to use each effectively.

Declaration Merging

One major unique capability of interfaces is declaration merging. This allows us to declare multiple interfaces with the same name, merging them into a single type:

interface Site {
  name: string;   
}

interface Site {
  url: string;  
}

const site: Site; // Site is { name: string, url: string }

Whereas types do not feature declaration merging – duplicately naming types will produce an error.

Extending Types

Both types and interfaces provide reuse of exiting types using the extends keyword.

Consider this interface-based approach:

interface BaseSite {
  name: string;
}  

interface Site extends BaseSite {
  url: string;  
}

interface SiteWithCounts extends Site {
  pageCount: number;
  postCount: number;
} 

We can easily build up new interfaces through extension.

For types, we have to leverage intersections to extend multiple types:

type BaseSite  = {
  name: string;  
};

type Site = BaseSite & {
  url: string;  
};

type SiteWithCounts = Site & {
  pageCount: number;
  postCount: number; 
};

So interfaces have more terse, flexible syntax for composing types through extension.

Structural vs Nominal Typing

One other key difference is that types define structural systems while interfaces create more nominal systems.

What does this mean?

In short, types care more about the shape of types matching whereas interfaces validate names matching.

For example, this type-based approach would succeed because the shapes match:

type Point = {
  x: number;
  y: number;  
};

function printPoint(point: { x: number; y: number }) {
  // ...
}

printPoint({x: 1, y: 2}); // ok

Even though we didn‘t annotate with the Point type directly, the anonymous object matches the structure expected.

However, with an interface-based approach this would fail:

interface Point {
  x: number;
  y: number;   
}

function printPoint(point: Point) { 
   // ...   
}

printPoint({x: 1, y: 2}) // Error, not of type Point

Here we care about the name – Point vs the anonymous object type.

When to Use Each Construct

Given their different strengths, when should we prefer using types vs interfaces? Here are some guidelines:

Use Types For:

  • Reusable building blocks
    • Ex: Tuples, typed arrays
  • Utility types for transformations
    • Ex: type Optional<T> = {[P in keyof T]+?}
  • Nested complex types and mappings

Use Interfaces For:

  • Contracts between classes
    • Ex: interface Saveable { save(): void; }
  • Polymorphic interfaces for inheritance
    • Ex: interface Renderable
  • Declaration merging object types together
  • Simple object extensions with extends

There are also stylistic preferences at play. Teams may adopt a convention consistently using one over the other unless needing specific capabilities.

Now let‘s walk through some advanced examples…

Advanced Concepts

We will explore several more advanced concepts to showcase types and interfaces working together effectively.

Generic Types

Both types and interfaces can be made generic using type parameters:

type SelectOption<T> = {
  label: string; 
  value: T;
};

interface SelectOption<T> {
  label: string;
  value: T;   
}; 

This allows them to be reusable for multiple type arguments.

Class Implementations

We can implement both types and interfaces using traditional classes:

With Type

type Site = {
  url: string;   
};

class MySite implements Site {
  constructor(public url: string) {}  
}

With Interface

interface Site {
  url: string;
}

class MySite implements Site {
  constructor(public url: string) {}  
}

So whether modeling using types or interfaces, classes can implement the contract.

React Component Patterns

Type aliases and interfaces really shine when utilized with React:

type AppProps = {  
  user: User;
  notifications: Notification[];  
};


interface AppState {
  isMenuOpen: boolean;  
}

Here we precisely model component props and state shape.

We can also leverage Discriminated Unions to model substitution control flow:

type LoginState = {
  status: "pending";  
};

type SuccessState = {
  status: "success";
  user: User;
};

type state = LoginState | SuccessState;

This shows how types excel at advanced modeling.

Troubleshooting TypeScript

Learning to debug type errors and issues is key. Here are some tips:

  • Leverage compiler flags like noImplicitAny and strictNullChecks
  • Use as assertions carefully for any workarounds
  • Prefer simpler approaches to complex mapped types
  • Let defaults work for you with liberal ? operators
  • Draw subsystem boundaries with any where needed

Taking time to master types pays off exponentially in long term app stability.

Key Takeaways

Typescript types and interfaces each unlock immense value:

  • Types enable complex reusable type blueprints
  • Interfaces focus on contracts between classes
  • Rely on types for composability, interfaces for simplicity
  • Leverage declaration merging and synthetic defaults
  • Model React state machines leveraging utility types

Learning the key differences allows smoothly incorporating types within JavaScript systems. Teams should establish guidelines tailored to their specific apps and architectures.

The future looks bright for typed JavaScript – buckle those seat belts, and happy typing!