typescriptclassinterface

What is the difference between defining interface outside and inside a class?


In my understanding, this is how you define interface inside class:

class A {
  a: number
  constructor() {
    this.a = 0
  }
}

And this is how you define interface outside class:

interface A {
  a: number
}
class A {
  constructor() {
    this.a = 0
  }
}

What is the difference between the two cases? When should I use one over the other? I notice that private and protected modifiers can't be used in interface, so if I need to use them then of course I need to use the inside case. But if I only use public, then it seems to be no difference between them?

Reading the documentation for Classes and Everyday Types doesn't help.


Solution

  • What you're calling "defining an interface inside a class" is just the normal way you write classes. Fields generally are declared in the class:

    class A {
      a: number
      constructor() {
        this.a = 0
      }
    }
    

    If you compile the above to target modern JavaScript and use the appropriate compiler options, it will produce a JavaScript class with a public class field:

    // JavaScript
    class A {
        a; // <-- public class field declaration
        constructor() {
            this.a = 0;
        }
    }
    

    On the other hand, what you're calling "defining an interface outside a class" is known as declaration merging. The interface merges with the class instance type, which sort of "patches" it at the type level. Interface declarations have absolutely no runtime effect, so anything you put in there that normally goes into the class declaration might behave differently. For example:

    interface B {
      a: number
    }
    class B {
      constructor() {
        this.a = 0
      }
    }
    

    If you compile that with the same compiler settings as above, you will get the following JavaScript, without a class field declaration:

    // JavaScript
    class B {
        // <-- no public class field declaration
        constructor() {
            this.a = 0;
        }
    }
    

    The fact that the two approaches can result in different JavaScript is an indication that they are not identical, although there's little pragmatic difference in this example. Still, you could easily change things where there are noticeable differences:

    class C { a?: number }
    console.log(Object.keys(new C()).length) // 1
    
    interface D { a?: number }
    class D { }
    console.log(Object.keys(new D()).length) // 0
    

    And certainly there are type checking differences. Interface merging adds stuff without having to pass the normal sorts of type checks:

    class E { a: number } // compiler error
    //        ~
    // Property 'a' has no initializer and is not definitely 
    // assigned in the constructor.
    new E().a.toFixed(); // runtime error
    
    interface F { a: number }
    class F { } // no compiler error
    new F().a.toFixed(); // runtime error
    

    In the above, E gives you a compiler error that you forgot to initialize a, whereas F does not give you that error, even though the same problem exists. So E warns you about an issue that F doesn't notice.


    Also, interfaces are not class declarations, so many class-specific things you can normally do are not available in interface merging. The most obvious example is that classes allow you to write runtime code, whereas interfaces do not. You can initialize class fields and implement class methods, but "initializing" an interface property or "implementing" an interface method doesn't make much sense:

    class G { a: number = 1; b(): number { return 2 } };
    
    interface H { a: number = 1, b(): number { return 2 }} // errors!
    

    Any class-specific modifiers like private or protected or static or abstract are unavailable in interfaces:

    abstract class I {
      abstract a: number;
      private b?: number;
      static c: number;
    }
    
    interface J {
      abstract a: number; // error!
      private b?: number; // error!
      static c: number; // error!
    }
    

    As for what you should do, this is at least somewhat subjective. In my opinion, declaration merging is an advanced technique that you only use to work around limitations in existing code. If you can do something without using it, you should.

    Playground link to code