Command Palette

Search for a command to run...

Hectal
PHASE 6Intermediate ~20 min· topic 4 of 8

Topic 6.4

Abstract Classes

In one line

An abstract class is a half-finished blueprint: it fixes the steps and shares the finished parts, but leaves some members abstract for each subclass to fill in. You cannot build an object from it directly, and the compiler makes sure every subclass completes it.

Think of it like this

A form with blanks, like a school report-card template. The school prints the layout: the logo, the table, the signature line. Some boxes are already filled (the school name) and some are blank (the child's name, the marks). You cannot hand in the blank template as a report card. Each teacher fills in the blanks for one student. An abstract class is that template: finished parts plus blanks that every subclass must fill.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Abstract class
A class that cannot be built directly and may contain members that subclasses must supply.
Abstract member
A method, property or accessor with no body, marked abstract. Subclasses must implement it.
Concrete
Fully implemented. A concrete class can be built with new.
Template method
A method in the base class that fixes the order of steps and calls pieces that subclasses provide.
Abstract constructor type
The type of an abstract class itself: new is not allowed on it.
Lifecycle
The fixed series of steps an object goes through, which the base class controls.
Contract
What a type promises: the members that must exist.

Step by step

01Fixed steps, blanks to fill

Report is abstract. title() and format are blanks. render() is finished and calls them, and body() has a default that a subclass may replace.

Sales fills format and title() and gets render() and body() for free.

report.tswhole filets
abstract class Report {
  abstract title(): string;
  abstract readonly format: string;

  render() {                 // template method: fixed steps
    return "[" + this.format + "] " + this.title() + "\n" + this.body();
  }

  protected body() {         // default a subclass may replace
    return "(empty)";
  }
}

class Sales extends Report {
  format = "pdf";
  title() {
    return "Sales";
  }
}
Fixed steps, blanks to filldiagram
Rendering diagram…

02You cannot build the template itself

new Report() is rejected: an abstract class exists to be extended. This also protects you from calling a method that has no body.

03An incomplete subclass is caught

If Circle extends Shape forgets area(), the error is on the class name and names both the missing member and the class it came from. Add the method or make Circle abstract too.

shape.tswhole filets
abstract class Shape {
  abstract area(): number;
  abstract name: string;
}
class Circle extends Shape {   // error TS2515: does not implement inherited abstract member area
  name = "circle";
}

04Abstract constructors in function arguments

A function that does new ctor() needs a concrete class. typeof Shape is abstract, so passing Shape is rejected; passing Sq (a concrete subclass) works. If your function only needs the class for instanceof or static members, type it as abstract new () => Shape.

05Abstract class or interface

Reach for an interface when many unrelated classes just need the same shape. Reach for an abstract class when subclasses share real code and the base class should control the order of steps.

Try it yourself

  1. 1

    Complete the circle

    In the second example, add area() { return 3 * this.r * this.r; } to Circle. Predict the Types panel, then add console.log(new Circle(2).describe()) and predict the output.

    Clean, and it prints circle area 12.0.

  2. 2

    Override the default body

    In the first example add protected body() { return "42 sales"; } inside Sales and predict the second console line. Then add override in front of it (it works because the parent has a body).

  3. 3

    Make Circle abstract instead

    Put abstract in front of class Circle in the second example. Is the error gone? Try new Circle(1).

    The class error disappears (an abstract class can leave blanks), and new Circle(1) is TS2511.

Code & diagrams

A template method with abstract members

Sales fills the two blanks and inherits render(). The last line is the compile error TS2511. The program still runs (it prints the report) because abstract only exists in the checker; the emitted class is an ordinary class.

Console

(nothing was printed)

Expected console output

[pdf] Sales
(empty)
A subclass that forgets an abstract member

Circle never wrote area(), so the error sits on its class line. Rect is complete and prints. Add area() { return 3 * this.r * this.r; } to Circle to fix.

Console

(nothing was printed)

Expected console output

rect area 6.0
Abstract classes and constructor types

make needs to run new, so only concrete classes qualify. describe only reads the class name, so abstract new () => Shape accepts abstract and concrete classes.

Console

(nothing was printed)

Expected console output

4
Shape Sq

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Instantiate the template

In the first example, keep the line const bad = new Report(); at the end.

terminal
$ Types panel
── what you'll see ──
index.ts(23,13): error TS2511: Cannot create an instance of an abstract class.

Myth vs fact

Myth

abstract means the class is empty.

Fact

An abstract class often has plenty of real code. It is abstract because it leaves some members for subclasses, and so cannot be built alone.

Myth

Abstract classes and interfaces are the same thing.

Fact

An interface is erased and holds no code. An abstract class exists at runtime, can hold code and state, and a class can extend only one.

Myth

abstract is enforced when the program runs.

Fact

It is a TypeScript-only check. The emitted JavaScript is a normal class, and untyped code can still call new on it.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    abstract is erased: nothing in the emitted JavaScript stops new Report(), and abstract members produce no code. If you need a runtime guard (for code that may be called from plain JavaScript), add if (new.target === Report) throw new Error(...) in the constructor.

  • ▸

    A subclass can implement an abstract property with a plain field (format = "pdf", the usual case) or with a getter, as long as the types agree.

  • ▸

    Calling an abstract method from the abstract class's constructor compiles but is a bug waiting to happen, because subclass fields are not set yet (Topic 6.3). TypeScript reports TS2715: Abstract property 'x' in class 'C' cannot be accessed in the constructor for abstract properties; abstract methods called there are your responsibility.

  • ▸

    abstract new () => T (an abstract construct signature) was added in TypeScript 4.2. It lets mixins and factories accept abstract classes safely.

Remember this

  1. 1

    The problem this solves: Sometimes a parent class has shared steps but can't finish the job itself, and a subclass could forget to supply the missing part. An abstract class makes the compiler demand it. You write abstract class Report { ... }. An abstract member has no body and the word abstract in front (abstract title(): string;). It is a promise: "every real subclass will provide this". A normal (concrete) member in the same class has a body and is inherited as is. An abstract class may mix both.

  2. 2

    Two rules make this useful. First, you can't create an object from the abstract class itself: new Report() is error TS2511: Cannot create an instance of an abstract class. Second, a concrete subclass must implement every abstract member it inherits, or TypeScript reports TS2515: Non-abstract class 'Circle' does not implement inherited abstract member area from class 'Shape'. A subclass that stays incomplete must itself be declared abstract.

  3. 3

    The most useful pattern is the template method: the abstract class has a normal method (render) that defines the steps of an algorithm and calls abstract or overridable pieces (title, body) in a fixed order. Subclasses fill in the pieces; nobody can change the order. Frameworks use this a lot: the base class owns the lifecycle, you supply the details.

  4. 4

    Abstract members can be methods, properties (abstract readonly format: string;) and accessors (abstract get id(): string). They can be public or protected but not private, because a private member can't be seen by the subclass that must implement it. Do not use abstract members in the abstract class's own constructor: the subclass has not run yet, the same order issue you saw in Topic 6.3 (the checker rejects reading an abstract property there, see the notes below).

  5. 5

    Abstract class or interface? Both describe what an object must have. An interface (Topic 3.2) is erased at runtime and holds no code; a class can implement many of them (Topic 6.5). An abstract class survives to runtime (so instanceof Report works), can hold shared code and state, and can have constructors and non-public members, but a class can extend only one parent. Rule of thumb: use an interface for "can do X", and an abstract class when subclasses really share code and a lifecycle.

  6. 6

    Types for classes themselves matter here: typeof Shape for an abstract class is an abstract constructor type. You can't pass it where a function type new () => Shape is needed, because that type promises new works. TypeScript reports Cannot assign an abstract constructor type to a non-abstract constructor type. To accept both concrete and abstract classes write abstract new () => Shape (an abstract construct signature).

Explain it without notes

01

What is an abstract class and what does the compiler enforce about it?

02

What is the template method pattern and how does an abstract class support it?

03

When would you choose an abstract class over an interface?

04

Why can't you pass an abstract class to a parameter typed new () => Shape?

Practice

01

Write an abstract class Notifier with an abstract channel: string, an abstract send(message: string): void and a concrete notify(message: string) that logs the channel and then calls send. Add EmailNotifier.

02

Add a second subclass SmsNotifier that forgets to implement send. Write down the error.

Trade-offs

  • ↔

    An abstract class gives you shared code and a controlled lifecycle but uses up the single extends slot. An interface keeps the slot free but shares nothing.

  • ↔

    Template methods make the order of steps hard to change by accident, but they hide control flow: readers must jump between base and child to follow one operation.

Done when you can

  • I can write an abstract class with abstract and concrete members.

  • I can explain TS2511 and TS2515.

  • I can write a template method and explain why it is useful.

  • I can choose between an abstract class and an interface.

  • I can explain why abstract new () => T exists.