Skip to content

Software Engineer at Polygon

TypeScript interface vs. type

I’m not an expert in the field of TypeScript by any means but I have worked with it every single day for the last few months and I am really enjoying the ride. Beneath its straight-forward set of features there are some confusing concepts as well. Should it be an interface or a type alias? This is a question asked a lot by newcomers.

interface DudeInterface {
  name: string;
  age: number;
}

const pawel: DudeInterface = {
  name: "Pawel",
  age: 31
};
type DudeType = {
  name: string,
  age: number
};

const pawel: DudeType = {
  name: "Pawel",
  age: 31
};

Both methods are correct to describe a structure of an object but which one should we use? As always — it depends. Let me compare and contrast them.

Misleading section of the official TypeScript Handbook #

The “Interfaces vs. Type Aliases” section of the official TypeScript Handbook explains the characteristics and differences between both of them.

  1. Interfaces create a new name, type aliases don’t
  2. Type aliases cannot be extended or implemented from

Since June 2016 when this part of the documentation was last updated, TypeScript has had a major version bump and lots of functionality has changed. Unfortunately none of these points are true anymore. It is a great time to update this obsolete part of the documentation. I will try to do a better job at explaining the difference. Hopefully the TypeScript Handbook will be updated eventually, then I will get rid of this section of the article.

Microsoft actively works on a brand new TypeScript Handbook that does a much better job at explaining the subject. It is a work in progress and we don’t know the date when it is going to replace the current Handbook.

Interfaces are restricted to an object type #

Interface declarations can exclusively represent the shape of an object-like data structures. Type alias declarations can create a name for all kind of types including primitives (undefined, null, boolean, string and number), union, and intersection types. In a way, this difference makes the type more flexible. In theory every type declaration that you can express with an interface, you can recreate using a type alias. Lets have a look at an example that can be represented using a type alias but is beyond the power of an interface.

type info = string | { name: string };

You can merge interfaces but not types #

Multiple declarations with the same name are valid only when used with interface. Doing so doesn’t override previous one but produces a merged result containing members from all declarations.

interface DudeInterface {
  name: string;
}

interface DudeInterface {
  age: number;
}

const pawel: DudeInterface = {
  name: "Pawel Grzybek",
  age: 31
};

Attempting to merge types results in a Duplicate identifier compiler error.

Compiler error caused by attempting to merge type aliases

Type aliases can use computed properties #

The in keyword can be used to iterate over all of the items in an union of keys. We can use this feature to programmatically generate mapped types. Have a look at this example using type aliases.

type Keys = "firstname" | "surname"

type DudeType = {
  [key in Keys]: string
}

const test: DudeType = {
  firstname: "Pawel",
  surname: "Grzybek"
}

Unfortunately we cannot take advantage of computed properties in an interface declaration.

Compiler error caused by using computed properties on an interface

Deferred type resolution of interfaces vs. eager type aliases #

This is no longer truth. Since I wrote this article, TypeScript behavior changed slightly and now the resolution of both (types and interfaces) happens in the same phase. Looks like both of them are deferred so the example from the image below is now perfectly valid TypeScript code.

Another difference is when a type is resolved by the compiler. Resolution of an interface is deferred, means that you can use them to recursively chain types. Resolution of type aliases is eager and compiler goes crazy when you try to resolve recursively nested types. Look!

type Dude = string | Pals;
interface Pals extends Array<Dude> {}

We are allowed to do it, because type of interfaces is deferred. Equivalent with type alias results with Type alias circularly references itself compiler error.

Recursively chained type aliases result in &ldquo;circularly references itself&rdquo; compiler error

Be consistent #

Just because in many situations you can use either of them, it doesn’t mean you should use them interchangeably. As an active open source contributor I see some advantages of using interface for authoring a public API and I tend to use it more often. The type alias is irreplaceable in some circumstances mentioned in this article. Most importantly — keep it consistent. Hopefully this article helped you out.

Comments

  • G
    Gregory Jarvez

    Insightful article Mr Pawel. I know when to use types now. 😉

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Thank you for kind words Mr Gregory :)

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • L
    Lubien

    Great read.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Thank you very much :)

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • N
    Nico Jansen

    Why are you waiting for the handbook to be updated. Just do it yourself: https://github.com/microsof...

    Also, great read. I think consensus amongst devs is to use `interface` whenever you can. It's in the default tslint ruleset.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      You are not the first one who suggested me to contribute to a handbook. You know what — I may give it a go :-)

      Thanks for your kind words and have a great day 🥑

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • N
        Nico Jansen

        I love avocado! Thanks! 🧡

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • A
    Anish George

    Good article. Thanks

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • D
    Daniel Lane

    Good stuff Pawel!

    An addition to this article that I think would make it a little better is to explain what an interface is and what a type is before comparing and contrasting them.

    Explain that an interface is a promise or contract to implement a data shape, you kinda touch on this but it's definitely worth further explanation. You should do the same for types as well, as a type is a definition of a type of data that can be declared and assigned.

    Many people use interfaces as types, which isn't strictly the correct way to use them, certainly not in other languages anyways.

    Using a type the correct way

    type Communication = string | { message: string };

    const comm1: Communication = 'lol';
    const comm2: Communication = { message: 'lol' };

    Using a type the wrong way

    type Contract = {
    message: string;
    print: Function;
    }
    // This is syntactically correct, but since when do people implement a type?
    // you declare and instantiate types, we implement interfaces as that's the purpose of polymorphism!
    class MessageContract implements Contract {
    constructor(public message: string) { }
    print() {
    console.log(this.message);
    }
    }

    Using an interface the correct way by defining an interface and implementing it in a class

    interface Contract {
    message: string;
    print: Function;
    }

    class Messager implements Contract {
    constructor(public message: string) { }
    print() {
    console.log(this.message);
    }
    }

    const messager = new Messager('Hello World!);
    messager.print();

    Using an interface the incorrect way by using it as a type

    interface Messager {
    print: Function;
    }

    const messager: Messager = {
    print: () => {
    console.log('Hello!')
    }
    }

    messager.print();

    I personally think the only other appropriate usage of an interface is to define the shape of arguments to a function as this is still technically a contract or promise of a data shape that the function expects.


    interface PropTypes {
    name: string;
    age: number;
    }

    const funkyFunc = (props: PropTypes) => {
    const { name, age } = props;
    console.log(`Hello ${name}, you're ${age} old!`);
    }

    Another key difference not expanded on enough imho is the deferred type resolution of interfaces. Interfaces can extend classes as well as types. The interface will inherit the properties and their types of a class when this is done which is super neat, I guess it's the point of what you're saying?

    Types can't do this at all.


    class Messager {
    sayHello() {
    console.log('Hello!);
    }
    }

    interface AwesomeMessager extends Messager {
    name: string;
    }

    class MessageLogger implements AwesomeMessager {
    constructor(public name: string) {}
    sayHello() {
    console.log(`Hello ${this.name}!`);
    }
    }

    Anyway, great article and keep 'em coming 💪

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Wow dude 😲

      Copy / paste = new article :-)

      Thanks for clarification.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • M
      Matthijs Wensveen

      I'm not sure I agree with the "Using an interface the incorrect way by using it as a type" part. This pattern:


      interface Foo { foo: string, bar: string }
      var o: Foo = { foo: "foo", bar: "bar" };

      is pretty common and used in a lot of places. If we should consider this as an anti-pattern, a lot of code would become more convoluted. So I would like a more detailed rationale.

      Maybe an alternative could be:

      var o: Foo = { foo: "foo", bar: "bar" } as Foo;

      Does the official TypeScript documentation say anything about this?

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • T
    TomsDisqusted

    Good article, thanks!

    Do you have any thoughts on the 'no-type-alias' rule that is on by default in typescript-eslint:
    https://github.com/typescri...

    Generally I avoid overriding default rules but after reading your article...

    Also, just a syntax issue, but shouldn't your type elements be deliniated with a semicolon instead of a comma (eg. for DudeType)?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Hi.

      I am not sure why this is a recommended rule. It would be worth to ask why author of this rule consider it as a bad practice.

      Re syntax, same like you I like defaults and this is a Prettier default setting and I keep using it. I have no other reason :)

      Thanks for reading !

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • T
        TomsDisqusted

        The comma vs. semicolon thing is an Typescript linting rule (member-delimiter-style). In a sense the 'author' of these is Microsoft because the linter I'm using (typescript-eslint) is now the official TS linter.

        Any of these rules can be overridden, of course, but it is worth being aware of them. And, for the record, another rule that is relevant to all this is 'prefer-interface'.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • Pawel Grzybek
          Pawel Grzybek

          The `member-delimiter-style` rule is one of those that came from TS Lint community. Microsoft has nothing to do with ESLint now, just officially announced that ESLint is the way to go.

          https://pawelgrzybek.com/li...

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
  • j
    jjjefff

    Thanks for the article! FYI, as of TypeScript v3.7, type allows circular references:

    type Dude = string | Pals;
    type Pals = Dude[];
    type Result = "valid" | "invalid" | Promise<Result>;

    Try it on the TS playground.

    Issue: https://github.com/microsof...

    PR: https://github.com/microsof...

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I skipped this news from TS 3.7 announcement article. Thanks a lot!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • j
    jeremychone

    Note with 3.8, this seems to work
    type Dude = string | Pals;
    type Pals = Dude[];

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • A
    André Kovac

    Could you explain a bit what it means when you say that the "resolution of an `interface` is deferred" and the "resolution of `type` aliases is eager? I coudn't find any article about how typescript resolves its `interface`s and `type` aliases.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Sorry for a late reply.

      This behavior of type resolution is no longer truth. It has been changed in one of the recent versions of TypeScript and now types and interfaces are resolved in the same compilation phase. Both of them are using deferred type resolutions.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • O
    OctKun

    I want to build a blog of your type. Is there any tutorial? Thank you very much.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • A
    Alan

    Thanks very much, I. think I'd learn a lot.

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!