DEV Community

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;
};
Enter fullscreen mode Exit fullscreen mode

Generics

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

type Animal<P = string> = {
  name: P;
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

implements

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

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

Extend classes

class Control {
  private state: any;
}

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

type SelectableControl = Control & {
  select: () => void;
};
Enter fullscreen mode Exit fullscreen mode

Functions

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

interface iBark {
  (x: Animal): void;
}
Enter fullscreen mode Exit fullscreen mode

and generics:

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

interface iBark {
  <P = Animal>(x: P): void;
}
Enter fullscreen mode Exit fullscreen mode

Recursive declarations

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

interface ITree<P> {
  node: P;
  leafs: ITree<P>[];
}
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

Indexable

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

interface IStringRecord {
  [index: string]: number;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Disjoint unions

Disjoint unions works only for types:

type DomesticAnimals = { type: "Dog" } | { type: "Cat" };
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

new

You can declare the type of new

interface IClassyAnimal {
  new (name: string);
}
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

Only one declaration per scope

You can declare types only once per scope

type Once = { a: string };
type Once = { b: string };
// Duplicate identifier 'Once'.
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (24)

Collapse
maxart2501 profile image
Massimo Artizzu • Edited

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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 🙂

Collapse
stereobooster profile image
stereobooster Author • Edited

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.

Collapse
penspinner profile image
Steven Liao • Edited
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.

Collapse
stereobooster profile image
Collapse
stereobooster profile image
stereobooster Author

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;
Collapse
stereobooster profile image
stereobooster Author

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?

Collapse
tevescastro profile image
Vítor Castro

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.

Collapse
stereobooster profile image
stereobooster Author

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

Thread Thread
tevescastro profile image
Vítor Castro • Edited

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.

Collapse
bolt04 profile image
David Pereira

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
}
Collapse
steida profile image
Daniel Steigerwald

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.

Collapse
d3vtoolsmith profile image
Petr Filipchyk

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

Collapse
stereobooster profile image
stereobooster Author • Edited

they look the same?

interface Animal {
  name: string;
}

replace interface with type add equal sign before {

type Animal = {
  name: string;
};
Collapse
d3vtoolsmith profile image
Petr Filipchyk

"=" 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.

Thread Thread
stereobooster profile image
stereobooster Author

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...

Thread Thread
tevescastro profile image
Vítor Castro

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.

Collapse
thitemple profile image
Thiago Temple

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.

Collapse
brianyang profile image
Brian Yang

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...

Collapse
michaeljota profile image
Michael De Abreu

In the new section, you wrote:

interface IClassyAnimal {
  new (name: string);
}

But it should be:

interface IClassyAnimal {
  new (name: string): this;
}
Collapse
stereobooster profile image
stereobooster Author • Edited

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;
  }
}
Collapse
michaeljota profile image
Michael De Abreu

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.

Thread Thread
stereobooster profile image
stereobooster Author

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)

Collapse
yogeswaran79 profile image
Yogeswaran

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

Forem Open with the Forem app