When you refer to "defining an interface inside a class," you are essentially describing the standard way of writing classes. Fields are typically declared within the class itself:
class A {
a: number
constructor() {
this.a = 0
}
}
If you compile the above code to target modern JavaScript with the correct compiler options, it will generate a JavaScript class featuring a public class field:
// JavaScript
class A {
a; // <-- public class field declaration
constructor() {
this.a = 0;
}
}
On the other hand, defining an interface outside a class is referred to as declaration merging. This process merges the interface with the class instance type, effectively "patching" it at the type level. However, any declarations within the interface do not impact the runtime behavior. For instance:
interface B {
a: number
}
class B {
constructor() {
this.a = 0
}
}
Compiling the above example using the same settings mentioned earlier will yield JavaScript without a class field declaration:
// JavaScript
class B {
// <-- no public class field declaration
constructor() {
this.a = 0;
}
}
The potential for different JavaScript outcomes from these two approaches suggests that they are not identical. Although in some cases, such as the one provided, the practical distinction may be minimal. However, there could be noticeable variations based on context:
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
Furthermore, there are differences in type checking between the two methods. Interface merging can introduce elements bypassing standard 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 given examples, class E
triggers a compiler error due to uninitialized property a
, while class F
does not. Therefore, E
detects an issue that F
overlooks.
Moreover, interfaces lack various class-specific features, like the ability to execute runtime code. Class fields initialization and method implementation are exclusive to classes and cannot be mirrored in interface merging:
class G { a: number = 1; b(): number { return 2 } };
interface H { a: number = 1, b(): number { return 2 }} // errors!
Modifiers such as private
, protected
, static
, or abstract
are not accessible 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!
}
Ultimately, whether to use declaration merging depends on individual preferences. Generally, it is considered an advanced technique aimed at overcoming limitations in existing code. If possible, opting for alternatives may be more straightforward.
Playground link to code