Wanna see something cool? Check out Angular Spotify 🎧

The different between type and interface in TypeScript

From Effective TypeScript: 62 Specific Ways to Improve Your TypeScript by Dan Vanderkam

It is a really good book. Please purchase the book to support its author!

If you are the publisher and think this article should not be public, please write me an email to trungk18 [at] gmail [dot] com and I will make it private.

If you want to define a named type in TypeScript, you have two options. You can use a type as show here:

type TState = {
  name: string;
  capital: string;
}

or an interface:

interface IState {
  name: string
  capital: string
}

You could also use a class, but that is a JavaScript runtime concept that also introduces a value.

Which should you use, type or interface? The line between these two options has become increasingly blurred over the years, to the point that in many situations you can use either. You should be aware of the distinctions that remain between type and interface and be consistent about which you use in which situation. But you should also know how to write the same types using both, so that you’ll be comfortable reading TypeScript that uses either.

Warning: The examples in this item prefix type names with I or T solely to indicate how they were defined. You should not do this in your code! Prefixing interface types with I is common in C#, and this convention made some inroads in the early days of TypeScript. But it is considered bad style today because it’s unnecessary, adds little value, and is not consistently followed in the standard libraries.

First, the similarities

The State types are nearly indistinguishable from one another

If you define an IState or a TState value with an extra property, the errors you get are character-by-character identical:

const wyoming: TState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
// ~~~~~~~~~~~~~~~~~~ Type ... is not assignable to type 'TState'
//                    Object literal may only specify known properties, and
//                    'population' does not exist in type 'TState'
};

You can use an index signature with both interface and type

type TDict = { [key: string]: string };
interface IDict {
  [key: string]: string;
}

You can also define function types with either

type TFn = (x: number) => string;
interface IFn {
  (x: number): string;
}

const toStrT: TFn = x => '' + x;  // OK
const toStrI: IFn = x => '' + x;  // OK

The type alias looks more natural for this straightforward function type, but if the type has properties as well, then the declarations start to look more alike:

type TFnWithProperties = {
  (x: number): number;
  prop: string;
}
interface IFnWithProperties {
  (x: number): number;
  prop: string;
}

You can remember this syntax by reminding yourself that in JavaScript, functions are callable objects.

Both type aliases and interfaces can be generic

type TPair<T> = {
  first: T;
  second: T;
}
interface IPair<T> {
  first: T;
  second: T;
}

An interface can extend a type (with some caveats, explained momentarily), and a type can extend an interface

interface IStateWithPop extends TState {
  population: number;
}
type TStateWithPop = IState & { population: number; };

Again, these types are identical. The caveat is that an interface cannot extend a complex type like a union type. If you want to do that, you’ll need to use type and &.

A class can implement either an interface or a simple type

class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}

Those are the similarities. What about the differences?

You’ve seen one already—there are union types but no union interfaces

type AorB = 'a' | 'b';

Extending union types can be useful. If you have separate types for Input and Output variables and a mapping from name to variable:

type Input = { /* ... */ };
type Output = { /* ... */ };
interface VariableMap {
  [name: string]: Input | Output;
}

then you might want a type that attaches the name to the variable. This would be:

type NamedVariable = (Input | Output) & { name: string };

This type cannot be expressed with interface.

A type is, in general, more capable than an interface. It can be a union, and it can also take advantage of more advanced features like mapped or conditional types.

Type can also more easily express tuple and array types

type Pair = [number, number];
type StringList = string[];
type NamedNums = [string, ...number[]];

You can express something like a tuple using interface:

interface Tuple {
  0: number;
  1: number;
  length: 2;
}
const t: Tuple = [10, 20];  // OK

But this is awkward and drops all the tuple methods like concat. Better to use a type.

An interface does have some abilities that a type doesn’t, however

One of these is that an interface can be augmented. Going back to the State example, you could have added a population field in another way:

interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 500_000
};  // OK

This is known as “declaration merging”, and it’s quite surprising if you’ve never seen it before. This is primarily used with type declaration files (Chapter 6), and if you’re writing one, you should follow the norms and use interface to support it. The idea is that there may be gaps in your type declarations that users need to fill, and this is how they do it.

TypeScript uses merging to get different types for the different versions of JavaScript’s standard library. The Array interface, for example, is defined in lib.es5.d.ts. By default this is all you get. But if you add ES2015 to the lib entry of your tsconfig.json, TypeScript will also include lib.es2015.d.ts. This includes another Array interface with additional methods like find that were added in ES2015. They get added to the other Array interface via merging. The net effect is that you get a single Array type with exactly the right methods.

Merging is supported in regular code as well as declarations, and you should be aware of the possibility. If it’s essential that no one ever augment your type, then use type.

Should you use type or interface?

For complex types, you have no choice: you need to use a type alias. But what about the simpler object types that can be represented either way? To answer this question, you should consider consistency and augmentation. Are you working in a codebase that consistently uses interface? Then stick with interface. Does it use type? Then use type.

For projects without an established style, you should think about augmentation. Are you publishing type declarations for an API? Then it might be helpful for your users to be able to be able to merge in new fields via an interface when the API changes. So use interface. But for a type that’s used internally in your project, declaration merging is likely to be a mistake. So prefer type.

Things to remember

  • Understand the differences and similarities between type and interface.
  • Know how to write the same types using either syntax.
  • In deciding which to use in your project, consider the established style and whether augmentation might be beneficial.
Published 27 Sep 2020

Read more

 — Angular Jira Clone Part 06 - Build a markdown text editor
 — Apply types to entire function expressions when possible
 — Angular Jira Clone Part 05 - Build an interactive drag and drop board
 — How to kill the process currently using a given port on Windows
 — Use VSCode Like a PRO

Follow @trungvose on Twitter for more!