| |

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 Shape as 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 Shape isn’t a thing you draw โ€” it’s an abstraction over Circle, Square, and others. Marking it abstract makes 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 Shape doesn’t care whether the argument is a Circle, a Square, or a plain object โ€” it only cares that the shape has area() and describe(). 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.

AspectAbstract ClassInterface
Runtime existenceโœ… YesโŒ No
Can be instantiatedโŒ NoN/A
Can have implementationโœ… YesโŒ No
Can have stateโœ… YesโŒ No
InheritanceSingle (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, the id field 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:

  • PaymentMethod interface โ€” the contract; any implementation can satisfy it
  • BasePaymentMethod abstract class โ€” shared state and helpers (id, validate, log)
  • CardPayment and PayPalPayment โ€” concrete implementations with specific logic
  • checkout โ€” 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

AspectAbstract ClassInterface
Keywordabstract classinterface
Runtime existenceโœ…โŒ
Can be instantiatedโŒN/A
Can have implementationโœ…โŒ
Can have stateโœ…โŒ
Can have constructorโœ…โŒ
Access modifiersโœ…โŒ
Inheritanceextends (single)implements (multiple)
Structural typingโŒโœ…
Declaration mergingโŒโœ…

Abstract Class Syntax

FeatureExample
Declarationabstract class Base { }
Abstract methodabstract area(): number;
Abstract propertyabstract id: string;
Concrete methoddescribe(): string { }
Constructorconstructor(name: string) { }
Access modifiersprotected, private, readonly
Subclassclass Child extends Base { }

Interface Syntax

FeatureExample
Declarationinterface Shape { }
Methodarea(): number;
Propertyid: string;
Optionalname?: string;
Readonlyreadonly id: string;
Index signature[key: string]: T;
Call signature(x: number): number;
Construct signaturenew (x: number): T;
Genericinterface Box<T> { }
Class implementsclass C implements I { }

When to Use Which

SituationUse
Shared implementationAbstract class
Shared stateAbstract class
Access modifiersAbstract class
Constructor logicAbstract class
Multiple contractsInterface
Unrelated classesInterface
Structural typingInterface
Object shapesInterface
Declaration mergingInterface
No runtime costInterface

Abstract Class Features

FeatureAvailable
Abstract methodsโœ…
Abstract propertiesโœ…
Concrete methodsโœ…
Concrete propertiesโœ…
Constructorโœ…
Access modifiersโœ…
Static membersโœ…
Multiple inheritanceโŒ

Interface Features

FeatureAvailable
Method signaturesโœ…
Property signaturesโœ…
Readonly membersโœ…
Optional membersโœ…
Index signaturesโœ…
Call signaturesโœ…
Construct signaturesโœ…
Generic parametersโœ…
ImplementationโŒ
ConstructorโŒ

Common Patterns

PatternExample
Template methodAbstract class with hooks
Contract-onlyInterface
Contract + shared implAbstract class implements interface
Multiple contractsClass implements many interfaces
StructuralInterface on plain objects
AugmentingInterface declaration merging
FactoryAbstract class with static factory

Errors and Messages

ErrorCause
Cannot create an instance of abstract classnew on abstract
Non-abstract class does not implement inherited abstract memberMissing implementation
Class incorrectly implements interfaceMissing member
Property 'x' is missing in typeInterface not satisfied

Abstract Class vs Concrete Class

AspectAbstractConcrete
InstantiableโŒโœ…
Abstract membersโœ… allowedโŒ
SubclassingRequiredOptional
PurposeBase for othersUsable directly

Type Compatibility

From โ†’ ToWorks
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

PitfallProblemSolution
new on abstract classCompile errorSubclass first
Missing abstract implementationCompile errorImplement in subclass
Missing super() in subclassCompile errorCall it first
Confusing implements and extendsType errorextends classes, implements interfaces
Interface with implementationSyntax errorMove impl to class
Abstract property without implCompile errorSubclass must set it
Using abstract class for multiple inheritanceNot possibleUse interfaces
Relying on interface at runtimeErasedUse abstract class
Forgetting overrideSilent overrideAdd override
Access modifier mismatchCompile errorMatch 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

AspectAbstract ClassInterface
PurposeBase with shared implementationContract
Runtimeโœ… ExistsโŒ Erased
InstantiableโŒN/A
Implementationโœ…โŒ
Stateโœ…โŒ
Constructorโœ…โŒ
Access modifiersโœ…โŒ
Inheritanceextends (single)implements (multiple)
Structural typingโŒโœ…
Declaration mergingโŒโœ…
Default choiceWhen shared logic neededMost 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!