It depends on whether you need code or a runtime value. Usually an interface is enough.
Interface (or type alias):
- Describes only the shape:
charge(amount: Money): Promise<ChargeId>. - It is erased when TypeScript compiles. Nothing exists at runtime.
- A class can implement many interfaces.
- Best for a pure contract. For an object shape, an interface and a type alias behave almost the same.
Abstract class:
- Can hold shared code: retries, logging, a template method that calls
abstract doCharge(). - It is a real JavaScript class at runtime. So
instanceofworks, and NestJS can use it as an injection token without@Inject. - A class can extend only one abstract class. So it ties each implementation to that base.
How I choose:
- Interface for the contract.
- Abstract class when the implementations really share logic, or when I want a runtime token in NestJS.
- Composition (inject a
RetryPolicy) instead of a base class when the shared code is only a helper.
- An interface is a compile-time shape and is erased at runtime.
- An abstract class exists at runtime and can contain shared code.
- A class can implement many interfaces but extend only one class.
- An abstract class can be a NestJS injection token, but an interface cannot.
- Prefer an interface, and use an abstract class only when there is real shared code or you need a runtime token.