TypeScript 21 ๐ท Abstract Classes and Interfaces
TypeScript gives you two ways to define a contract that other classes must fulfill: abstract classes and interfaces. They overlap in purpose โ both describe a shape that implementations must satisfy โ but they work differently and suit different situations. An abstract class is a real class that can’t be instantiated, can hold implementation, and can have abstract members that subclasses must implement. An interface is a purely structural contract with no runtime existence โ it describes what an object must have, and any class or object that matches the shape satisfies it. Knowing when to use each is one of the practical skills that separates TypeScript developers who model problems well from those who don’t.
Key point: An abstract class is a class โ it exists at runtime, can hold state, can have implemented methods, and uses extends for a single inheritance chain. An interface is a type โ it has no runtime footprint, is purely structural, and can be implemented by many unrelated classes. Use abstract classes when you want to share implementation among related classes; use interfaces when you want to describe a contract that any class (related or not) can fulfill.
What an abstract class is
An abstract class is a class declared with the abstract keyword. It can’t be instantiated directly โ you must subclass it and (usually) implement its abstract members.
abstract class Shape {
abstract area(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
Shape can’t be instantiated โ new Shape() is a compile error. It declares an abstract method area() that subclasses must implement, and a concrete method describe() that uses area(). The abstract class provides both a contract and shared behavior.
Subclassing:
class Circle extends Shape {
constructor(private radius: number) {
super();
}
override area(): number {
return Math.PI * this.radius ** 2;
}
}
class Square extends Shape {
constructor(private side: number) {
super();
}
override area(): number {
return this.side ** 2;
}
}
new Circle(5).describe(); // "Area: 78.54"
new Square(4).describe(); // "Area: 16.00"
Circle and Square both extend Shape, implement area(), and inherit describe(). The abstract class enforces the contract (must implement area) and provides shared logic (describe).
What abstract classes can contain:
- Abstract methods โ declared but not implemented; subclasses must implement them
- Abstract properties โ declared but not initialized; subclasses must set them
- Concrete methods โ fully implemented and inherited
- Concrete properties โ initialized and shared
- Access modifiers โ
public,private,protected,readonly - Constructors โ run when subclasses instantiate
What abstract classes can’t do:
- Be instantiated directly (
new Shape()fails) - Be used as a type without extending (you can use
Shapeas a type, but only instances of subclasses are assignable)
Why abstract classes exist: Some base classes only make sense as a foundation for subclasses. A
Shapeisn’t a thing you draw โ it’s an abstraction overCircle,Square, and others. Marking itabstractmakes that intent explicit and prevents accidental instantiation. It also lets you declare methods that subclasses must implement, without providing a default that would be wrong for all cases.
What an interface is
An interface is a purely structural type. It has no runtime existence โ it describes the shape that an object must have.
interface Shape {
area(): number;
describe(): string;
}
Any object with area() returning a number and describe() returning a string satisfies Shape. The interface has no implementation, no state, and no runtime footprint.
Class implements interface:
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
Circle implements Shape. The compiler checks that all members are present with matching types. Unlike extends, implements doesn’t create an inheritance relationship โ it just asserts that the class matches the interface.
Object literal as interface:
const point: Shape = {
area: () => 0,
describe: () => 'point'
};
Any object with the right shape satisfies the interface. No class required.
What interfaces can contain:
- Method signatures
- Property types
- Readonly properties
- Optional members (
?) - Index signatures
- Call signatures
- Construct signatures
- Generic parameters
What interfaces can’t contain:
- Implementations
- Initialized properties
- Constructors with bodies
- Access modifiers (mostly)
- Runtime code
Why interfaces exist: They describe contracts without implementation. A function that accepts a
Shapedoesn’t care whether the argument is aCircle, aSquare, or a plain object โ it only cares that the shape hasarea()anddescribe(). This is structural typing โ matching by shape, not by inheritance. It’s the most flexible way to describe what an object needs to do.
Key differences
The two concepts overlap but differ in fundamental ways.
| Aspect | Abstract Class | Interface |
|---|---|---|
| Runtime existence | โ Yes | โ No |
| Can be instantiated | โ No | N/A |
| Can have implementation | โ Yes | โ No |
| Can have state | โ Yes | โ No |
| Inheritance | Single (extends) | Many (implements) |
| Access modifiers | โ Yes | โ No |
| Constructor | โ Yes | โ No |
| Structural typing | โ Nominal | โ Structural |
| Declaration merging | โ No | โ Yes |
| Runtime footprint | โ Class object | โ Erased |
Runtime: An abstract class is a real class โ it appears in the emitted JavaScript. An interface disappears entirely. This matters for bundle size, for instanceof checks, and for anything that needs the class at runtime.
Inheritance: A class can extend only one abstract class but implement many interfaces. This is the classic “single inheritance, multiple interfaces” model from Java and C#.
Structural typing: An interface is satisfied by any object with the right shape โ no inheritance needed. An abstract class requires explicit extends.
Declaration merging: Interfaces can be merged across declarations; abstract classes can’t.
Access modifiers: Abstract classes can have private, protected, readonly. Interfaces describe public shape only.
Which is more flexible: Interfaces. Any object with the right shape satisfies them. Abstract classes require the class hierarchy.
Why both exist: They serve different purposes. Abstract classes are for sharing implementation among related classes. Interfaces are for describing contracts that any class or object can satisfy. Sometimes you want both โ an interface for the contract, an abstract class that provides a default implementation. That’s the “abstract class implements interface” pattern.
When to use an abstract class
An abstract class is the right tool when:
You want to share implementation:
abstract class Logger {
abstract format(msg: string): string;
log(msg: string): void {
console.log(this.format(msg));
}
}
class JsonLogger extends Logger {
override format(msg: string): string {
return JSON.stringify({ message: msg });
}
}
class PlainLogger extends Logger {
override format(msg: string): string {
return msg;
}
}
Logger provides log(), which both subclasses inherit. Each subclass customizes only format(). That’s the template method pattern โ a shared algorithm with customizable steps.
You have state shared among subclasses:
abstract class Entity {
protected id: string;
protected createdAt: Date;
constructor() {
this.id = crypto.randomUUID();
this.createdAt = new Date();
}
}
class User extends Entity {
constructor(public name: string) { super(); }
}
class Product extends Entity {
constructor(public title: string) { super(); }
}
Both User and Product inherit id and createdAt. The abstract class holds the shared state and initialization logic.
You need access modifiers:
abstract class Base {
protected helper(): void { }
private secret(): void { }
}
Interfaces don’t have access modifiers. If you need protected or private, you need a class.
You want to enforce super() calls:
abstract class Base {
constructor() {
// initialization
}
}
class Child extends Base {
constructor() {
super(); // must call
}
}
Abstract classes have constructors; subclasses must call super(). This enforces initialization order.
When abstract classes are wrong: When you don’t need shared implementation, when the classes aren’t related, or when structural typing is enough. If all you need is a contract, an interface is simpler and more flexible.
Why not always use abstract classes: They lock in a single inheritance chain and require
extends. If you need multiple contracts, or if the classes don’t share implementation, an interface is better. Abstract classes are for when there’s genuinely shared behavior โ not just a shared shape.
When to use an interface
An interface is the right tool when:
You want to describe a contract:
interface Serializable {
serialize(): string;
}
function save(obj: Serializable): void {
fs.writeFileSync('data.json', obj.serialize());
}
save accepts anything with serialize(). Any class or object that matches the shape works โ no inheritance required.
Multiple unrelated classes need the same contract:
class User implements Serializable {
serialize(): string { return JSON.stringify(this); }
}
class Config implements Serializable {
serialize(): string { return JSON.stringify(this); }
}
class Cache implements Serializable {
serialize(): string { return JSON.stringify(this); }
}
Three unrelated classes, all Serializable. No shared base class โ just a shared contract.
You want structural typing:
interface Point {
x: number;
y: number;
}
function distance(a: Point, b: Point): number {
return Math.hypot(a.x - b.x, a.y - b.y);
}
distance({ x: 0, y: 0 }, { x: 3, y: 4 }); // โ
object literals work
The function accepts any object with x and y. No class, no inheritance โ just a shape.
You want to define object shapes:
interface User {
id: number;
name: string;
email?: string;
}
const alice: User = { id: 1, name: 'Alice' };
Interfaces describe object shapes for data. This is their most common use โ not just contracts for classes.
You want declaration merging:
interface Window {
myApp: MyApp;
}
interface Window {
analytics: Analytics;
}
Two declarations merge into one. Libraries use this to augment global types.
When interfaces are wrong: When you need shared implementation, state, or access modifiers. Those require an abstract class.
Why interfaces are the modern default: They’re more flexible (structural, multiple), have no runtime cost, and describe contracts independently of class hierarchies. Most TypeScript code uses interfaces for shapes and contracts, and abstract classes only when there’s genuine shared behavior. The rule of thumb: interface first, abstract class when you need implementation.
Abstract class implements interface
A common pattern: define a contract as an interface, then provide a partial implementation as an abstract class.
interface Repository<T> {
get(id: string): Promise<T | null>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
abstract class BaseRepository<T> implements Repository<T> {
abstract get(id: string): Promise<T | null>;
async save(item: T): Promise<void> {
const id = (item as any).id;
await this.write(id, item);
}
async delete(id: string): Promise<void> {
await this.write(id, null);
}
protected abstract write(id: string, item: T | null): Promise<void>;
}
class UserRepository extends BaseRepository<User> {
async get(id: string): Promise<User | null> {
return this.fetch(id);
}
protected async write(id: string, item: User | null): Promise<void> {
// persist to storage
}
private async fetch(id: string): Promise<User | null> {
// load from storage
return null;
}
}
Repository<T> is the contract. BaseRepository<T> implements the parts that don’t vary (save, delete) and declares the parts that do (get, write) as abstract. UserRepository implements the abstract methods.
Why this pattern: The interface is the public contract โ any implementation can satisfy it, not just subclasses of BaseRepository. The abstract class provides shared implementation for the common case. Subclasses get the shared behavior without redeclaring it.
The benefit: Consumers depend on the interface, not the base class. You can swap BaseRepository for any other implementation that satisfies the interface, without changing consumers.
Why this pattern is idiomatic: It separates the contract (interface) from the implementation (abstract class). The interface is what consumers care about; the abstract class is one way to implement it. When you need flexibility โ say, a mock repository in tests โ you can implement the interface without extending the base class.
Abstract properties
Abstract classes can declare abstract properties that subclasses must implement.
abstract class Entity {
abstract id: string;
abstract createdAt: Date;
describe(): string {
return `Entity ${this.id}`;
}
}
class User extends Entity {
id = crypto.randomUUID();
createdAt = new Date();
constructor(public name: string) { super(); }
}
Entity declares id and createdAt as abstract. Subclasses must provide them. The abstract class can use this.id in describe() โ subclasses guarantee it’s set.
Abstract properties with access modifiers:
abstract class Base {
protected abstract secret: string;
}
class Derived extends Base {
protected secret = 'value';
}
Subclasses must match the access modifier โ protected abstract requires protected implementation.
Readonly abstract properties:
abstract class Base {
abstract readonly id: string;
}
class Derived extends Base {
readonly id = crypto.randomUUID();
}
The abstract property is readonly, so subclasses must declare it readonly.
Why abstract properties exist: Sometimes a base class needs a property but can’t provide a default. The abstract declaration says “subclasses must provide this.” It’s the property equivalent of an abstract method.
Why abstract properties matter: In a base class like
Entity, theidfield is critical but can’t be defaulted โ each subclass might generate it differently. Declaring it abstract forces subclasses to provide it and lets the base class use it safely.
Abstract classes and interfaces together
The two are complementary. A well-designed hierarchy often uses both.
Interface for the contract:
interface Comparable<T> {
compareTo(other: T): number;
}
Abstract class for shared behavior:
abstract class BaseEntity implements Comparable<BaseEntity> {
abstract id: string;
compareTo(other: BaseEntity): number {
return this.id.localeCompare(other.id);
}
}
Concrete subclasses:
class User extends BaseEntity {
id = crypto.randomUUID();
}
class Product extends BaseEntity {
id = crypto.randomUUID();
}
Comparable<T> is the contract. BaseEntity implements it with shared logic. Subclasses inherit the behavior. Any code that needs Comparable accepts User, Product, or any other implementation โ not just subclasses.
The layering:
Interface (contract)
โฒ
โ implements
โ
Abstract class (shared implementation)
โฒ
โ extends
โ
Concrete classes (specific behavior)
This is the standard pattern in languages that support both โ Java, C#, and TypeScript.
Why both: The interface decouples consumers from the class hierarchy. The abstract class reduces duplication among related implementations. You get flexibility at the consumer level and efficiency at the implementation level.
Why not just use the abstract class: If consumers depend on the abstract class, they can’t accept other implementations โ like mocks in tests or alternative implementations in the future. Depending on the interface instead keeps the consumer flexible. The abstract class is an implementation detail; the interface is the contract.
A full example
A payment processing system with interfaces and abstract classes.
// ============================================
// INTERFACES โ CONTRACTS
// ============================================
interface PaymentMethod {
readonly id: string;
readonly type: string;
process(amount: number): Promise<PaymentResult>;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
}
// ============================================
// ABSTRACT BASE CLASS
// ============================================
abstract class BasePaymentMethod implements PaymentMethod {
abstract readonly type: string;
readonly id: string;
protected readonly name: string;
constructor(name: string) {
this.id = crypto.randomUUID();
this.name = name;
}
abstract process(amount: number): Promise<PaymentResult>;
protected validate(amount: number): void {
if (amount <= 0) throw new Error('Amount must be positive');
if (amount > 100_000) throw new Error('Amount exceeds limit');
}
protected log(msg: string): void {
console.log(`[${this.name}] ${msg}`);
}
}
// ============================================
// CONCRETE IMPLEMENTATIONS
// ============================================
class CardPayment extends BasePaymentMethod {
readonly type = 'card';
constructor(
private cardNumber: string,
private expiry: string
) {
super('CardPayment');
}
async process(amount: number): Promise<PaymentResult> {
this.validate(amount);
this.log(`Charging card ending ${this.cardNumber.slice(-4)}`);
// Simulated API call
return {
success: true,
transactionId: crypto.randomUUID()
};
}
}
class PayPalPayment extends BasePaymentMethod {
readonly type = 'paypal';
constructor(private email: string) {
super('PayPalPayment');
}
async process(amount: number): Promise<PaymentResult> {
this.validate(amount);
this.log(`Charging ${this.email}`);
return {
success: true,
transactionId: crypto.randomUUID()
};
}
}
// ============================================
// USAGE
// ============================================
async function checkout(
method: PaymentMethod,
amount: number
): Promise<void> {
const result = await method.process(amount);
if (result.success) {
console.log(`Paid $${amount} via ${method.type}`);
} else {
console.error(`Payment failed: ${result.error}`);
}
}
const card = new CardPayment('4242424242424242', '12/25');
const paypal = new PayPalPayment('user@example.com');
await checkout(card, 99.99);
await checkout(paypal, 49.5);
What this shows:
PaymentMethodinterface โ the contract; any implementation can satisfy itBasePaymentMethodabstract class โ shared state and helpers (id,validate,log)CardPaymentandPayPalPaymentโ concrete implementations with specific logiccheckoutโ depends on the interface, not the class hierarchy
The interface decouples the consumer. The abstract class reduces duplication.
Why this shape: It’s how real payment systems are modeled. The interface is the public API. The abstract class provides common plumbing. Concrete classes handle the specifics. Consumers accept any implementation โ including mocks in tests or a new provider later.
Complete Example Session
# ============================================
# PART 1: ABSTRACT CLASS
# ============================================
cat > abstract.ts << 'EOF'
abstract class Shape {
abstract area(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) { super(); }
override area(): number {
return Math.PI * this.radius ** 2;
}
}
class Square extends Shape {
constructor(private side: number) { super(); }
override area(): number {
return this.side ** 2;
}
}
console.log(new Circle(5).describe());
console.log(new Square(4).describe());
// new Shape(); // โ can't instantiate
EOF
npx tsc --noEmit abstract.ts
# (no errors)
# ============================================
# PART 2: INTERFACE
# ============================================
cat > interface.ts << 'EOF'
interface Shape {
area(): number;
describe(): string;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
describe(): string {
return `Area: ${this.area().toFixed(2)}`;
}
}
// Structural typing โ object literals work
const point: Shape = {
area: () => 0,
describe: () => 'point'
};
console.log(new Circle(5).describe());
console.log(point.describe());
EOF
npx tsc --noEmit interface.ts
# (no errors)
# ============================================
# PART 3: TRIGGER ERRORS
# ============================================
cat > errors.ts << 'EOF'
interface Shape {
area(): number;
describe(): string;
}
// โ Missing describe
class Bad implements Shape {
area(): number { return 0; }
}
abstract class Base {
abstract value: number;
}
// โ Missing value
class Child extends Base {
// value: number = 0;
}
EOF
npx tsc --noEmit errors.ts
# [ errors.ts:6:7 - Class 'Bad' incorrectly implements interface 'Shape'. ]
# [ errors.ts:6:7 - Property 'describe' is missing in type 'Bad' but required in type 'Shape'. ]
# [ errors.ts:15:7 - Non-abstract class 'Child' does not implement inherited abstract member 'value' from class 'Base'. ]
rm errors.ts
# ============================================
# PART 4: ABSTRACT CLASS IMPLEMENTS INTERFACE
# ============================================
cat > both.ts << 'EOF'
interface Repository<T> {
get(id: string): Promise<T | null>;
save(item: T): Promise<void>;
}
abstract class BaseRepo<T> implements Repository<T> {
abstract get(id: string): Promise<T | null>;
async save(item: T): Promise<void> {
console.log('Saving', item);
}
}
class UserRepo extends BaseRepo<{ id: string; name: string }> {
async get(id: string) {
return { id, name: 'Alice' };
}
}
const repo = new UserRepo();
repo.get('1').then(u => console.log(u));
EOF
npx tsc --noEmit both.ts
# (no errors)
# ============================================
# PART 5: ABSTRACT PROPERTIES
# ============================================
cat > props.ts << 'EOF'
abstract class Entity {
abstract readonly id: string;
describe(): string {
return `Entity ${this.id}`;
}
}
class User extends Entity {
readonly id = crypto.randomUUID();
constructor(public name: string) { super(); }
}
console.log(new User('Alice').describe());
EOF
npx tsc --noEmit props.ts
# (no errors)
# ============================================
# PART 6: FULL EXAMPLE
# ============================================
cat > payment.ts << 'EOF'
interface PaymentResult {
success: boolean;
transactionId?: string;
error?: string;
}
interface PaymentMethod {
readonly id: string;
readonly type: string;
process(amount: number): Promise<PaymentResult>;
}
abstract class BasePayment implements PaymentMethod {
abstract readonly type: string;
readonly id = crypto.randomUUID();
protected readonly name: string;
constructor(name: string) { this.name = name; }
abstract process(amount: number): Promise<PaymentResult>;
protected validate(amount: number): void {
if (amount <= 0) throw new Error('Amount must be positive');
}
}
class CardPayment extends BasePayment {
readonly type = 'card';
constructor(private last4: string) { super('Card'); }
async process(amount: number): Promise<PaymentResult> {
this.validate(amount);
return { success: true, transactionId: crypto.randomUUID() };
}
}
class PayPalPayment extends BasePayment {
readonly type = 'paypal';
constructor(private email: string) { super('PayPal'); }
async process(amount: number): Promise<PaymentResult> {
this.validate(amount);
return { success: true, transactionId: crypto.randomUUID() };
}
}
async function checkout(m: PaymentMethod, amount: number): Promise<void> {
const r = await m.process(amount);
console.log(r.success ? `Paid $${amount} via ${m.type}` : 'Failed');
}
(async () => {
await checkout(new CardPayment('4242'), 99.99);
await checkout(new PayPalPayment('user@example.com'), 49.5);
})();
EOF
npx tsc --noEmit payment.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc abstract.ts interface.ts both.ts props.ts payment.ts
node abstract.js
# [ Area: 78.54 ]
# [ Area: 16.00 ]
node interface.js
# [ Area: 78.54 ]
# [ point ]
node props.js
# [ Entity <uuid> ]
node payment.js
# [ Paid $99.99 via card ]
# [ Paid $49.5 via paypal ]
Quick Reference
Abstract Class vs Interface
| Aspect | Abstract Class | Interface |
|---|---|---|
| Keyword | abstract class | interface |
| Runtime existence | โ | โ |
| Can be instantiated | โ | N/A |
| Can have implementation | โ | โ |
| Can have state | โ | โ |
| Can have constructor | โ | โ |
| Access modifiers | โ | โ |
| Inheritance | extends (single) | implements (multiple) |
| Structural typing | โ | โ |
| Declaration merging | โ | โ |
Abstract Class Syntax
| Feature | Example |
|---|---|
| Declaration | abstract class Base { } |
| Abstract method | abstract area(): number; |
| Abstract property | abstract id: string; |
| Concrete method | describe(): string { } |
| Constructor | constructor(name: string) { } |
| Access modifiers | protected, private, readonly |
| Subclass | class Child extends Base { } |
Interface Syntax
| Feature | Example |
|---|---|
| Declaration | interface Shape { } |
| Method | area(): number; |
| Property | id: string; |
| Optional | name?: string; |
| Readonly | readonly id: string; |
| Index signature | [key: string]: T; |
| Call signature | (x: number): number; |
| Construct signature | new (x: number): T; |
| Generic | interface Box<T> { } |
| Class implements | class C implements I { } |
When to Use Which
| Situation | Use |
|---|---|
| Shared implementation | Abstract class |
| Shared state | Abstract class |
| Access modifiers | Abstract class |
| Constructor logic | Abstract class |
| Multiple contracts | Interface |
| Unrelated classes | Interface |
| Structural typing | Interface |
| Object shapes | Interface |
| Declaration merging | Interface |
| No runtime cost | Interface |
Abstract Class Features
| Feature | Available |
|---|---|
| Abstract methods | โ |
| Abstract properties | โ |
| Concrete methods | โ |
| Concrete properties | โ |
| Constructor | โ |
| Access modifiers | โ |
| Static members | โ |
| Multiple inheritance | โ |
Interface Features
| Feature | Available |
|---|---|
| Method signatures | โ |
| Property signatures | โ |
| Readonly members | โ |
| Optional members | โ |
| Index signatures | โ |
| Call signatures | โ |
| Construct signatures | โ |
| Generic parameters | โ |
| Implementation | โ |
| Constructor | โ |
Common Patterns
| Pattern | Example |
|---|---|
| Template method | Abstract class with hooks |
| Contract-only | Interface |
| Contract + shared impl | Abstract class implements interface |
| Multiple contracts | Class implements many interfaces |
| Structural | Interface on plain objects |
| Augmenting | Interface declaration merging |
| Factory | Abstract class with static factory |
Errors and Messages
| Error | Cause |
|---|---|
Cannot create an instance of abstract class | new on abstract |
Non-abstract class does not implement inherited abstract member | Missing implementation |
Class incorrectly implements interface | Missing member |
Property 'x' is missing in type | Interface not satisfied |
Abstract Class vs Concrete Class
| Aspect | Abstract | Concrete |
|---|---|---|
| Instantiable | โ | โ |
| Abstract members | โ allowed | โ |
| Subclassing | Required | Optional |
| Purpose | Base for others | Usable directly |
Type Compatibility
| From โ To | Works |
|---|---|
| Subclass โ Abstract | โ |
| Abstract โ Subclass | โ |
| Class โ Interface | โ (if shape matches) |
| Object โ Interface | โ (if shape matches) |
| Interface โ Class | โ |
Best Practices
โ Do This:
// Use abstract classes for shared implementation
abstract class Shape {
abstract area(): number;
describe(): string { return `Area: ${this.area()}`; }
} // โ
// Use interfaces for contracts
interface Serializable {
serialize(): string;
} // โ
// Combine both โ interface for contract, abstract class for impl
interface Repository<T> {
get(id: string): Promise<T | null>;
}
abstract class BaseRepository<T> implements Repository<T> {
abstract get(id: string): Promise<T | null>;
} // โ
// Use abstract properties for required fields
abstract class Entity {
abstract id: string;
} // โ
// Use protected for subclass helpers
abstract class Base {
protected log(msg: string): void { }
} // โ
// Depend on interfaces, not classes
function save(obj: Serializable): void { } // โ
// Mark overrides explicitly
class C extends Base {
override method(): void { }
} // โ
โ Don’t Do This:
// Don't use abstract class when an interface suffices
abstract class Serializable {
abstract serialize(): string;
}
// No shared implementation โ use interface // โ ๏ธ
// Don't try to instantiate abstract classes
new Shape(); // โ compile error // โ
// Don't use an abstract class just for one method
abstract class OnlyOneMethod {
abstract doIt(): void;
}
// Interface is simpler // โ ๏ธ
// Don't skip `implements` when a class matches an interface
// Implicit matching works, but explicit is clearer // โ ๏ธ
// Don't use abstract classes to enforce single inheritance
// when multiple contracts are needed // โ
// Don't add implementation to interfaces (not possible)
interface Bad {
method() { } // โ syntax error // โ
}
// Don't forget `super()` in subclass constructors
class Child extends Base {
constructor() {
// super(); // โ required // โ
}
}
// Don't confuse `implements` with `extends`
class C extends SomeInterface { } // โ interfaces aren't classes // โ
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
new on abstract class | Compile error | Subclass first |
| Missing abstract implementation | Compile error | Implement in subclass |
Missing super() in subclass | Compile error | Call it first |
Confusing implements and extends | Type error | extends classes, implements interfaces |
| Interface with implementation | Syntax error | Move impl to class |
| Abstract property without impl | Compile error | Subclass must set it |
| Using abstract class for multiple inheritance | Not possible | Use interfaces |
| Relying on interface at runtime | Erased | Use abstract class |
Forgetting override | Silent override | Add override |
| Access modifier mismatch | Compile error | Match parent |
Real-World Examples
1. Abstract class with abstract method
abstract class Shape {
abstract area(): number;
}
2. Concrete method in abstract class
abstract class Shape {
abstract area(): number;
describe(): string { return `Area: ${this.area()}`; }
}
3. Abstract property
abstract class Entity {
abstract id: string;
}
4. Abstract readonly property
abstract class Entity {
abstract readonly id: string;
}
5. Constructor in abstract class
abstract class Base {
constructor(public name: string) {}
}
6. Protected helper
abstract class Base {
protected log(msg: string): void { }
}
7. Subclass implementation
class Circle extends Shape {
constructor(private r: number) { super(); }
override area(): number { return Math.PI * this.r ** 2; }
}
8. Interface contract
interface Serializable {
serialize(): string;
}
9. Class implements interface
class User implements Serializable {
serialize(): string { return JSON.stringify(this); }
}
10. Multiple interfaces
class User implements Serializable, Comparable<User> { }
11. Structural interface
interface Point { x: number; y: number; }
const p: Point = { x: 0, y: 0 };
12. Optional interface member
interface User {
id: number;
nickname?: string;
}
13. Readonly interface member
interface User {
readonly id: number;
}
14. Index signature
interface Dict {
[key: string]: number;
}
15. Call signature
interface Logger {
(msg: string): void;
}
16. Generic interface
interface Box<T> {
value: T;
}
17. Abstract class implements interface
abstract class Base implements Serializable {
abstract serialize(): string;
}
18. Interface extends interface
interface Admin extends User {
permissions: string[];
}
19. Class extends class implements interface
class Admin extends User implements Serializable {
serialize(): string { return JSON.stringify(this); }
}
20. Template method pattern
abstract class DataProcessor {
process(data: string[]): string[] {
return this.sort(this.transform(data));
}
protected abstract transform(data: string[]): string[];
protected sort(data: string[]): string[] { return [...data].sort(); }
}
Visual: Abstract Class vs Interface
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Abstract Class โ
โ โ
โ abstract class Shape { โ
โ abstract area(): number; โ
โ describe(): string { } โ
โ } โ
โ โ
โ โ
Runtime exists โ
โ โ
Shared implementation โ
โ โ
State โ
โ โ
Access modifiers โ
โ โ Single inheritance only โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Interface โ
โ โ
โ interface Shape { โ
โ area(): number; โ
โ describe(): string; โ
โ } โ
โ โ
โ โ No runtime existence โ
โ โ No implementation โ
โ โ No state โ
โ โ
Multiple implementation โ
โ โ
Structural typing โ
โ โ
Declaration merging โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Inheritance Model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Abstract class โ single inheritance โ
โ โ
โ Base โ
โ โฒ โ
โ โ extends โ
โ Child (only one parent) โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Interfaces โ multiple implementation โ
โ โ
โ Contract1 Contract2 Contract3 โ
โ โฒ โฒ โฒ โ
โ โโโโโโโโโโโโโผโโโโโโโโโโโโโ โ
โ โ implements โ
โ Class โ
โ โ
โ A class can implement many interfaces โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Abstract Class Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ abstract class Shape { โ
โ โ
โ abstract area(): number; โ
โ // โ must be implemented by subclass โ
โ โ
โ describe(): string { โ
โ return `Area: ${this.area()}`; โ
โ } โ
โ // โ shared implementation โ
โ } โ
โ โ
โ Cannot be instantiated โ
โ Subclasses must implement `area` โ
โ Subclasses inherit `describe` โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Interface Structure
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ interface Shape { โ
โ area(): number; โ
โ describe(): string; โ
โ } โ
โ โ
โ No implementation โ
โ No runtime โ
โ Any matching shape satisfies it โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Satisfied by: โ
โ โ
โ class Circle implements Shape { } โ
โ class Square implements Shape { } โ
โ const obj: Shape = { area, describe }; โ
โ โ
โ Any of these works โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Combined Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ interface Repository<T> { โ
โ get(id: string): Promise<T | null>; โ
โ save(item: T): Promise<void>; โ
โ } โ
โ // โ contract โ what consumers depend on โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ implements
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ abstract class BaseRepo<T> โ
โ implements Repository<T> { โ
โ โ
โ abstract get(id): Promise<T | null>; โ
โ โ
โ async save(item: T): Promise<void> { โ
โ // shared implementation โ
โ } โ
โ } โ
โ // โ shared code โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ extends
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class UserRepo extends BaseRepo<User> { โ
โ async get(id: string) { โ
โ // specific implementation โ
โ } โ
โ } โ
โ // โ specific behavior โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Runtime vs Type-Only
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Abstract class โ real at runtime โ
โ โ
โ class Shape { โ
โ describe() { ... } โ
โ } โ
โ โ
โ โ emitted JavaScript โ
โ โ appears in bundle โ
โ โ can be referenced at runtime โ
โ โ instanceof works โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Interface โ type-only โ
โ โ
โ interface Shape { ... } โ
โ โ
โ โ erased at compile โ
โ โ no runtime cost โ
โ โ can't be referenced at runtime โ
โ โ instanceof impossible โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: When to Use Which
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Need shared implementation? โ
โ โ โ
โ โโโ Yes โโโบ Abstract class โ
โ โ โ
โ โโโ No โ
โ โ โ
โ โโโ Need multiple contracts? โ
โ โ โ โ
โ โ โโโ Yes โโโบ Interface โ
โ โ โ โ
โ โ โโโ No โโโบ Interface โ
โ โ โ
โ โโโ Structural typing? โ
โ โ โ
โ โโโ Yes โโโบ Interface โ
โ โ โ
โ โโโ No โโโบ Interface โ
โ โ
โ Default: interface. Class only when shared. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Template Method Pattern
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ abstract class DataProcessor { โ
โ โ
โ process(data: string[]): string[] { โ
โ const filtered = this.filter(data); โ
โ const transformed = this.transform(filtered)โ
โ return this.sort(transformed); โ
โ } โ
โ // โ fixed algorithm โ
โ โ
โ protected filter(d): string[] { return d; }โ
โ protected transform(d): string[] { return d; }โ
โ protected sort(d): string[] { return [...d].sort(); }โ
โ // โ overridable hooks โ
โ } โ
โ โ
โ Subclasses change steps, not the flow โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Error Cases
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ new Shape() โ
โ // abstract class โ โ compile error โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Bad implements Shape { } โ
โ // missing members โ โ compile error โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Child extends AbstractBase { } โ
โ // missing abstract impl โ โ compile error โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ class Child extends Base { โ
โ constructor() { โ
โ this.x = 1; // โ super() first โ
โ super(); โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Visual: Decision Summary
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Use ABSTRACT CLASS when: โ
โ โ
โ โ
You have shared implementation โ
โ โ
You need protected/private members โ
โ โ
You have state to share โ
โ โ
You want a constructor โ
โ โ
Related classes need shared logic โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Use INTERFACE when: โ
โ โ
โ โ
You only need a contract โ
โ โ
Multiple unrelated classes implement โ
โ โ
You want structural typing โ
โ โ
You need declaration merging โ
โ โ
You want no runtime cost โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Use BOTH when: โ
โ โ
โ โ
Interface is the public contract โ
โ โ
Abstract class provides shared impl โ
โ โ
Subclasses get shared behavior โ
โ โ
Consumers depend on interface โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Aspect | Abstract Class | Interface |
|---|---|---|
| Purpose | Base with shared implementation | Contract |
| Runtime | โ Exists | โ Erased |
| Instantiable | โ | N/A |
| Implementation | โ | โ |
| State | โ | โ |
| Constructor | โ | โ |
| Access modifiers | โ | โ |
| Inheritance | extends (single) | implements (multiple) |
| Structural typing | โ | โ |
| Declaration merging | โ | โ |
| Default choice | When shared logic needed | Most cases |
Key takeaways:
- An abstract class can’t be instantiated and can declare abstract members subclasses must implement
- An interface is a purely structural contract with no runtime existence
- Abstract classes can hold implementation, state, constructors, and access modifiers
- Interfaces support multiple implementation, structural typing, and declaration merging
- Classes can extend one abstract class but implement many interfaces
- Use abstract classes when you have shared implementation among related classes
- Use interfaces when you need a contract that any class or object can satisfy
- The “abstract class implements interface” pattern separates contract from implementation
- Abstract properties force subclasses to provide values the base class needs
- Template method pattern โ abstract class defines the algorithm, subclasses fill in the steps
- Abstract classes have a runtime footprint; interfaces are erased
- Prefer interfaces by default โ reach for abstract classes only when you need shared implementation
Remember: Abstract classes and interfaces look similar but serve different purposes. An interface is a contract โ it says “anything with this shape works.” An abstract class is a base โ it says “extend me and fill in the blanks.” Interfaces are more flexible and have no runtime cost; abstract classes share implementation and state. Use interfaces for contracts, abstract classes for shared behavior, and combine them when you want both. That’s the whole skill โ knowing which tool fits the problem.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!