Start learning free

LESSON 189 OF 348 · OBJECT-ORIENTED PROGRAMMING

Abstract Classes in TypeScript: abstract Classes and Methods

Lesson Read the full explanation

TL ; DR:

An abstract class is a class you can't instantiate directly — it's a half-finished template. It can declare methods with no body (abstract methods) that any concrete subclass must implement.


class Animal {
    speak() { throw new Error("Not implemented"); }
}
class Dog extends Animal {
    speak() { console.log("Woof"); }
}


Full lesson:

Sometimes you want to capture shared behavior across a family of classes without committing to one concrete implementation. For example, every Animal can speak(), but exactly how depends on whether it's a Dog or a Cat. That's what abstract classes are for.

Two superpowers

  1. You can't write new Animal() directly — the compiler/runtime stops you. You have to pick a concrete subclass.
  2. Any subclass MUST implement every abstract method, or the compiler/runtime complains. The abstract class is a contract with teeth.

Mixing abstract and concrete

An abstract class can also have concrete methods (with bodies) and fields. Those are shared by all subclasses for free, just like normal inheritance. Only the methods marked abstract require an override.

Notes by language

  • C# / Java — keyword abstract on the class and on the method.
  • Python — inherit from abc.ABC and decorate the method with @abstractmethod.
  • JavaScript — no built-in support. The convention is a parent class whose method throws an error if not overridden.
  • C++ — a method ending with = 0 is "pure virtual"; a class with any pure virtual method is abstract.

Your task

The abstract Animal class is already defined. Create a concrete Dog subclass and implement the speak method so it prints "Woof". The harness builds a Dog and calls speak().

Expected output:

Woof
STEP 1 OF 2
    Ctrl + Enter to check

    TYPESCRIPT · OBJECT-ORIENTED PROGRAMMING

    Abstract Classes in TypeScript, explained

    What an abstract class is

    An abstract class is a class that exists to be extended, never to be created directly. It describes what a family of classes has in common and leaves some parts open for each member of the family to fill in. Think of Animal: every animal can speak(), but what that sounds like depends on whether it is a dog or a cat. There is no sensible way to speak as "just an animal".

    TypeScript gives you the abstract keyword for this. You put it on the class, and on every method that subclasses must provide:

    abstract class Animal {
        abstract speak(): void;
    }

    An abstract method has a signature and no body, just a semicolon. The signature is the contract: the name, the parameters and the return type that every concrete subclass has to match.

    The two rules the compiler enforces

    1. You can't create an instance of an abstract class. Writing new Animal() gives the error Cannot create an instance of an abstract class.
    2. A concrete subclass must implement every abstract method it inherits. If a class extends Animal and doesn't define speak, the compiler reports Non-abstract class 'Cat' does not implement inherited abstract member speak from class 'Animal'.

    Together these make the abstract class a contract with teeth. Any code that receives an Animal can call speak() knowing that a real implementation exists behind it.

    The lesson's exercise

    The starter code already declares the abstract Animal class and, at the bottom, two lines that create a Dog named rex and call its speak method. Your job is to write the missing Dog class so that running the program prints Woof.

    1. Declare Dog with extends Animal. The code at the bottom needs a class it can instantiate, and Animal itself is off limits, so the program needs a concrete subclass.
    2. Implement speak(): void inside it. The method must match the abstract signature: same name, no parameters, and a void return type. Its body logs the text with console.log.

    The finished program:

    abstract class Animal {
        abstract speak(): void;
    }
    
    class Dog extends Animal {
        speak(): void {
            console.log("Woof");
        }
    }
    
    const rex = new Dog();
    rex.speak();

    It prints:

    Woof

    Notice that Dog declares no constructor. A subclass without one inherits its parent's constructor, and Animal has none of its own, so new Dog() works with no arguments.

    Mixing abstract and concrete members

    An abstract class doesn't have to be all blanks. It can hold fields, a constructor and ordinary methods with bodies, and every subclass inherits them. Only the members marked abstract need an override. This is where abstract classes earn their keep: the shared code lives in one place, and the parts that vary are forced into the subclasses.

    abstract class Shape {
        constructor(public name: string) {}
    
        abstract area(): number;
    
        describe(): string {
            return `${this.name} with area ${this.area()}`;
        }
    }
    
    class Square extends Shape {
        constructor(private side: number) {
            super("Square");
        }
    
        area(): number {
            return this.side * this.side;
        }
    }
    
    console.log(new Square(3).describe());

    This prints:

    Square with area 9

    describe() is written once, in Shape, and calls area(), which each subclass defines in its own way. Because Square declares a constructor, it must call super(...) first to run the parent's constructor. Here public name and private side in the parameter lists are TypeScript shorthand for declaring a field and assigning it from the argument.

    What happens at run time

    The abstract keyword exists only for the compiler. When TypeScript turns your code into JavaScript, the modifier is removed, and so are the abstract method declarations. The output has a normal class Animal with no speak in it, and nothing in it stops anyone from creating one.

    That has two consequences worth knowing:

    • The compiler reports the error for new Animal(), but by default it still writes the JavaScript file. If that file runs, the object gets created. Treat compiler errors as real errors and fix them.
    • Type checks like these protect you only while the code is checked. If an instance of an incomplete class slips through, for example via a cast with as any, the failure shows up later as speak is not a function, because the method never existed in the generated JavaScript.

    If you need a hard guarantee at run time, you can check in the constructor with new.target, or throw an error from a base-class method. For most code, the compile-time check is the point of the feature.

    Common mistakes

    • Forgetting to implement an abstract method. The class fails to compile with the "does not implement inherited abstract member" error. Add the method, or mark the new class abstract too if it is meant to stay incomplete.
    • Using a different signature. If speak is declared to return void and a subclass's version needs a parameter the contract doesn't have, callers holding an Animal can't use it safely, so the compiler objects.
    • Giving an abstract method a body. abstract speak() { ... } is an error. A body belongs in the subclass.
    • Calling new on the abstract class. Instantiate a subclass, then store it in a variable typed as the abstract class if you want code that works for all of them: const pet: Animal = new Dog();.
    • Forgetting super(). A subclass that defines its own constructor must call super(...) before using this.

    Key takeaways

    • An abstract class is a base class you can extend but not instantiate.
    • An abstract method has a signature and no body; each concrete subclass must implement it.
    • Abstract classes can also carry fields and ordinary methods that every subclass inherits.
    • The checks happen at compile time. The generated JavaScript has no trace of abstract.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free