loading...
Cover image for TypeScript: type vs interface

TypeScript: type vs interface

stereobooster profile image stereobooster Originally published at stereobooster.com on ・3 min read

In one of my recent PRs I changed all interfaces to types because there were already more types than interfaces. In the review, I was asked to revert the change. I did it, but as well I wondered what the actual difference between interface and type. Let's figure out this. I use the latest TS (v3.5.1) for examples in this post.

Similarities

Records

interface IAnimal {
  name: string;
}

type Animal = {
  name: string;
};

Generics

interface IAnimal<P = string> {
  name: P;
}

type Animal<P = string> = {
  name: P;
};

Intersections

type Robot = {
  power: number;
};

interface IRobot {
  name: string;
}

interface IRoboAnimal1 extends IAnimal, IRobot {}
interface IRoboAnimal2 extends IAnimal, Robot {}
interface IRoboAnimal3 extends Animal, IRobot {}
interface IRoboAnimal4 extends Animal, Robot {}

type RoboAnimal1 = Animal & Robot;
type RoboAnimal2 = Animal & IRobot;
type RoboAnimal3 = IAnimal & Robot;
type RoboAnimal4 = IAnimal & IRobot;

implements

class Dog implements IAnimal {
  name: string = "good dog";
}

class Cat implements Animal {
  name: string = "Where is my food, human?";
}

Extend classes

class Control {
  private state: any;
}

interface ISelectableControl extends Control {
  select(): void;
}

type SelectableControl = Control & {
  select: () => void;
};

Functions

type Bark = (x: Animal) => void;

interface iBark {
  (x: Animal): void;
}

and generics:

type Bark = <P = Animal>(x: P) => void;

interface iBark {
  <P = Animal>(x: P): void;
}

Recursive declarations

type Tree<P> = {
  node: P;
  leafs: Tree<P>[];
};

interface ITree<P> {
  node: P;
  leafs: ITree<P>[];
}

Exact

type Close = { a: string };
const x: Close = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'Close'.

interface IClose {
  a: string;
}
const y: IClose = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'IClose'.

Indexable

type StringRecord = {
  [index: string]: number;
};

interface IStringRecord {
  [index: string]: number;
}

Differences

Primitive types

You can use only types to alias primitive types

type NewNumber = number;

interface INewNumber extends number {}
// 'number' only refers to a type, but is being used as a value here.

// this works
interface INewNumber extends Number {}
// but don't forget that 1 instanceof Number === false;

Tuples

You can't declare tuples with interfaces

type Tuple = [number, number];

interface ITuple {
  0: number;
  1: number;
}

[1, 2, 3] as Tuple; // Conversion of type '[number, number, number]' to type '[number, number]' may be a mistake

[1, 2, 3] as ITuple; // Ok

Disjoint unions

Disjoint unions works only for types:

type DomesticAnimals = { type: "Dog" } | { type: "Cat" };

And you can't use disjoint union types with extends

interface IDomesticAnimals extends DomesticAnimals {}
// An interface can only extend an object type or intersection of object types with statically known members

new

You can declare the type of new

interface IClassyAnimal {
  new (name: string);
}

it doesn't work as you expect

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
//  Type 'Parrot' provides no match for the signature 'new (name: string): void'.

constructor doesn't seem to work either

interface IClassyAnimal {
  constructor(name: string): void;
}

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
//  Types of property 'constructor' are incompatible.
//    Type 'Function' is not assignable to type '(name: string) => void'.
//      Type 'Function' provides no match for the signature '(name: string): void'.

Only one declaration per scope

You can declare types only once per scope

type Once = { a: string };
type Once = { b: string };
// Duplicate identifier 'Once'.

you can declare interface more than once per scope (the final result will be the sum of all declarations)

interface IOnce {
  a: string;
}
interface IOnce {
  b: string;
}

Utility types

Most of the time you would use types instead of interfaces to create utility types, for example:

export type NonUndefined<A> = A extends undefined ? never : A;

Conclusion

Not all of those things were possible in early versions of TS, so people got used to interfaces. But in the latest version of TS, it seems that types are more capable and we can always use them 🤔. Or I miss something?

There are a lot of nuances in TS - something may work for a small example (which I showed), but broken for big ones. Please correct me if I missed something.

Dedicated to @thekitze.

Posted on by:

stereobooster profile

stereobooster

@stereobooster

Hello, I'm a full stack web developer. Follow me on Twitter!

Discussion

markdown guide
 
type TAnimal = 'Dog' | 'Cat'

/*
A computed property name in an interface must refer to an expression whose type is a literal type or a 'unique symbol' type.
A computed property name must be of type 'string', 'number', 'symbol', or 'any'.
'TAnimal' only refers to a type, but is being used as a value here.
*/
interface IAnimals {
  [a in TAnimal]: string
}

// This works.
type TAnimals = {
  [a in TAnimal]: string
}

But there is Record<TAnimal, string> for this.

 
 

types are actually aliases

One of the sudden realizations I had about types and interfaces in TypeScript is that they're not actually "types", they're type aliases. As they don't define new types, they just they give them a new name.

In other words, they could have used the keyword alias instead of type and maybe saved some troubles.

This means that interfaces are "opaque" relatively to its internal structure, whereas type aliases are not. I.e., the type hint you get is just interface IAnimal in the former case, and the whole type alias definition in the latter.

Using new to define constructor signatures

My suggestion here is that you're misunderstanding the role of interface that define constructor signatures. When you do

interface IClassyAnimal {
  new (name: string): IAnimal;
}

that is not an interface you should implement. That's a type to describe the class itself. For example:

class Parrot {
  constructor(public name: string) {}
}

function petFactory(petClass: IClassyAnimal, name: string) {
  return new petClass(name);
}
const pet = petFactory(Parrot, 'McParrotface'); // this is fine

This means that if you have static properties defined on a class, you can have them defined on an interface:

interface IClassyAnimal {
  new (name: string): IAnimal;
  group: string;
}
const petClass: IClassyAnimal = class Parrot {
  static group = 'Bird';
  constructor(public name: string) {}
}

On the other hand, when you're using implements in a class, you're describing the shape of an instance of that class. Which means you write class Parrot implements IAnimal {...}, because Parrot instances comply to the shape defined by IAnimal.

Edit: missed one of your replies in the comments that is actually on point on that 🙂

 

the type hint you get is just interface IAnimal in the former case, and the whole type alias definition in the latter.

this is interesting. I saw it somewhere before but didn't pay attention, now I see what this was about.

 

One more thing: as I said interface allows extension even after declaration, so it would be possible extend interface exposed by 3-rd party library, but I can't imagine good use case for it. Any ideas?

 

There’s an example of the usefulness of this in a older than the current version of a functional programming library fp-ts.
It’s called declaration merging and it was used in that library, that implements higher kinded types, to extend those types.

 

Can you please point me to the code (link to the file/commit in github)? I want to understand it better

Yes, you can have a look here:
github.com/gcanti/fp-ts/tree/1.x

Has I explained this particular feature was used extensively in V1, but I think that is no longer the case due to the way the library is now structured.

 

An example where you'd want to extend the interface of a 3rd party API is OrderCloud. They have the property xp to enable you to extend their data model. Here's an example of a type where xp is object (I think sometimes it's any too).
And the extension would be something like:

interface OrderExtended extends Order {
    xp: OrderXp | null
}
export interface OrderXp {
  isCool: boolean
  isAwesome: boolean
  maybeIsBoth: boolean
}
 

Function overloading seems to work similar

let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x: any): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

interface IPickCard {
    (x: {suit: string; card: number; }[]): number
    (x: number): {suit: string; card: number; }
}

type PickCard = 
  | ((x: {suit: string; card: number; }[]) => number)
  | ((x: number) => {suit: string; card: number; });

const y: PickCard = pickCard;
const x: IPickCard = pickCard;
 

From TS docs:

“As we mentioned, type aliases can act sort of like interfaces; however, there are some subtle differences.

One difference is that interfaces create a new name that is used everywhere. Type aliases don’t create a new name — for instance, error messages won’t use the alias name. In the code below, hovering over interfaced in an editor will show that it returns an Interface, but will show that aliased returns object literal type.“

This is why we prefer interfaces. It makes everything more readable.

 

Type syntax seems archaic and obscure, Interfaces are more direct and explicit...clarity wins

 

I don’t agree. I think Type is simple and clear enough. Maybe this is more of a personal preference shaped by level of exposure and what you’re used to...🤔.

Whatever you prefer, I think it makes sense to stay consistent and use one of them.

 

they look the same?

interface Animal {
  name: string;
}

replace interface with type add equal sign before {

type Animal = {
  name: string;
};
 

"=" is already odd, different from classes and all other non-object "{ ... }" statements.
Also, almost no non-advanced training material I can remember talks about "type"..
Running a Dev shop, why overload people's brains with something that looks archaic and has questionable use (at least for business applications)?
Also everyone has lots of historic interface/class/type/structure luggage from other languages that doesn't map into using types in place of interfaces.
I imagine there's valid use for it for dev tool creators.

You keep using term "archaic".

a) What is your reason behind it?
b) Why archaic is a bad thing? Math notation of plus is archaic, let's change the notation?

I feel like people who have background in Java, C# would be more comfortable with notation of interface.
People who have background in functional languages, like OCaml (all ML family?) will prefer type notation.
People who don't have background - will accept whatever you show them first.

Argument "=" is already odd is a matter of taste. And if you don't like it based on that, there is no reason for me to argue. There are people who prefer to put semicolons in the end of JS and who don't...

I would be willing to bet that despite their different capabilities (types vs interfaces), people with an OOP background will be a bit biased towards interfaces while people with FP backgrounds will tend to prefer types.

 

I also disagree with this, I think for one an interface in the OO world conveys the message that somewhere you're going to have an implementation of sorts and when you don't have at least one of those that is not being explicit.

I would also argue that an interface conveys the message of abstraction which is not the case when one would use it to define the structure of an object which a type does a much better of.

I think a type is very explicit in that sense.

Also, you say that type is archaic, well, it's not, it is used in many other languages (modern languages) to represent exactly that.

 

In the new section, you wrote:

interface IClassyAnimal {
  new (name: string);
}

But it should be:

interface IClassyAnimal {
  new (name: string): this;
}
 

Doesn't seem to help typescriptlang.org/play/#code/JYOw... 🤔

interface IClassyAnimal {
  new (name: string): this;
}

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
//   Type 'Parrot' provides no match for the signature 'new (name: string): this'.

I guess the only reasonable usage of new is this:

interface ComesFromString {
    name: string;
}

interface StringConstructable {
    new(n: string): ComesFromString;
}

class MadeFromString implements ComesFromString {
    constructor (public name: string) {
        console.log('ctor invoked');
    }
}

function makeObj(n: StringConstructable) {
    return new n('hello!');
}

code example from stackoverflow.com/questions/134070...

If you want to declare type of constructor you can do something like this:

declare class IClassyAnimal {
  constructor(props: string);
}

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
 

Yes. I wasn't unable to check it, but I was sure I did something like that. But according to the docs

typescriptlang.org/docs/handbook/i...

Seems like you are right. Constructors interfaces declarations are mostly effective as function arguments.

I don't know all the answers, I simply experimented to write this post. It also can happen I miss a lot of things, or there are bugs in TS or some small details which I miss. All ideas are more than welcome. (TypeScript documentation seems to lag behind actual behavior sometimes)

 

I had a similar experience, which led me to open a PR for the Typescript handbook, documenting using type aliases over interfaces.

github.com/microsoft/TypeScript-Ha...

 

Hey there! I shared your article here t.me/theprogrammersclub and check out the group if you haven't already!