Suggest an editImprove this articleRefine the answer for “Abstract classes in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Abstract class** in TypeScript is a class that cannot be instantiated directly and serves as a base for subclasses, combining shared concrete methods with abstract methods that subclasses must implement. ```typescript abstract class Shape { abstract area(): number; describe(): string { return `Area: ${this.area().toFixed(2)}`; } } class Circle extends Shape { area(): number { return Math.PI * 5 ** 2; } // 78.54 } ``` **Key:** provides shared implementation alongside a contract, unlike interfaces which are contracts only.Shown above the full answer for quick recall.Answer (EN)Image**Abstract class** in TypeScript is a class you cannot instantiate directly. It acts as a shared base for subclasses, combining concrete methods (real implementation every subclass inherits) with abstract methods (required overrides each subclass must fill in). One place for both the contract and the shared code. ## Theory ### TL;DR - Blueprint analogy: defines required rooms (abstract methods) and pre-built walls (concrete methods), but you cannot live in the blueprint itself - Core difference from interface: abstract class provides partial implementation alongside contracts; interfaces are contracts only, no code - TypeScript enforces abstract members at compile time; at runtime V8 sees a plain ES6 class - Decision rule: use when 2+ subclasses share real implementation logic; use interface when you only need a structural shape ### Quick example ```typescript abstract class Animal { // Concrete: shared by every subclass automatically move(): string { return `${this.sound()} while moving`; } // Abstract: each subclass must implement this abstract sound(): string; } class Dog extends Animal { sound(): string { return 'Woof!'; } } const dog = new Dog(); dog.move(); // 'Woof! while moving' // new Animal(); // Error: Cannot create an instance of an abstract class ``` `move()` lives once in the base. Every subclass gets it without extra code. `sound()` forces each subclass to define its own behavior. That is the entire point. ### Abstract class vs interface Both describe shape. Only abstract classes carry code. An interface declares what a type looks like: method signatures, property names, return types. Nothing runs at runtime because TypeScript erases interfaces completely during compilation. An abstract class, by contrast, does compile to JavaScript. Its concrete methods become real class methods. Its abstract members become required overrides that TypeScript checks before emitting any JS. The practical split: if two or more classes need shared logic, use an abstract class. If you only need a structural contract (for function parameters, return types, or decoupled modules), use an interface. Most codebases use both together. | Feature | Abstract class | Interface | |---|---|---| | Concrete implementation | Yes | No | | Constructor | Yes | No | | Access modifiers | Yes (`public`, `protected`, `private`) | No | | Properties with values | Yes | No | | Multiple inheritance | No (single `extends`) | Yes (multiple `implements`) | | Exists at runtime | Yes (compiles to JS class) | No (erased at compile time) | ### When to use - Shared logic across subclasses: a base `Repository` with a concrete `findById` that all repos inherit - Template method pattern: define the algorithm skeleton in the base, let subclasses fill specific steps - Hierarchy with state: you need properties, constructors, and methods together in one base - Factory patterns: a base class for dynamically creating and returning subclass instances Skip abstract classes when you only need a contract (interface works), when the class is used in exactly one place, or when you are writing functional React code where interfaces compose more naturally. ### How TypeScript compiles abstract classes The TypeScript compiler (`tsc`) strips the `abstract` keyword and emits a standard ES6 class. Concrete methods become regular class methods. Abstract member declarations disappear entirely. The compiler checks that every subclass implements all abstract members before generating any JS output. At runtime, V8 or Node sees nothing special. The enforcement is purely TypeScript, gone by the time your code actually runs. ### Common mistakes **1. Trying to instantiate the abstract class directly** ```typescript abstract class A { abstract greet(): string; } const a = new A(); // TS2511: Cannot create an instance of an abstract class ``` Fix: extend it and implement the abstract method, then instantiate the subclass. **2. Forgetting to implement all abstract members** ```typescript abstract class A { abstract greet(): string; abstract farewell(): string; } class B extends A { greet() { return 'Hi'; } // Missing farewell() -> TS2515: Non-abstract class 'B' does not implement... } ``` Fix: implement every abstract member, or mark `B` itself as `abstract`. **3. Exposing a protected abstract override as public unintentionally** ```typescript abstract class Base { protected abstract init(): void; } class Derived extends Base { public init(): void { console.log('ready'); } // TypeScript allows widening to public, but... } ``` This compiles. But it exposes an internal lifecycle hook as part of the public API. Users of `Derived` can now call `init()` directly, which breaks encapsulation. Keep the override `protected` unless you deliberately want it public. **4. Constructor order when abstract methods are called from the base constructor** ```typescript abstract class Base { protected abstract init(): void; constructor() { this.init(); } // Calls subclass override during base construction } class Derived extends Base { protected value = 42; protected init(): void { this.value *= 2; } } const d = new Derived(); console.log((d as any).value); // 84 ``` This surprises developers who assume subclass field initializers run before the base constructor. They do not. `init()` fires inside the base `constructor()` call, before `value = 42` gets a chance to assign. I have seen this break logic inside a NestJS base controller where the team expected a default property to be set before `init` ran. The fix is to avoid calling abstract methods inside constructors whenever you can. **5. Using abstract classes in purely functional contexts** In React code built around hooks, interfaces are lighter and compose better. Adding an abstract class where an interface would do brings extra weight without any real benefit. ### Real-world usage - NestJS: controllers and services extend abstract base classes with shared validation and decorator logic - TypeORM: `BaseEntity` provides concrete `save()`, `remove()`, and `find()` methods that every entity inherits - Express: a base middleware class with `abstract handle(req, res)` forces each route class to define its own handler ### Follow-up questions **Q:** What does the JavaScript output of an abstract class look like? **A:** A plain ES6 class. The `abstract` keyword and all abstract member declarations are stripped. Only concrete methods remain. The structural check happens entirely at compile time. **Q:** Can abstract properties exist in TypeScript? **A:** Yes. `abstract color: string;` in a base class forces the subclass to assign that property, either as a direct field or through a getter. **Q:** Is there a runtime performance difference between abstract classes and interfaces? **A:** No. Interfaces disappear at compile time. Abstract classes become regular JS classes. Both have identical runtime cost. **Q:** Can you declare a private abstract method? **A:** No, TypeScript throws an error. Abstract methods must be `protected` or `public` so subclasses can override them. **Q:** Why can a class use `extends` on an abstract class but not `implements` it? **A:** TypeScript uses structural typing for interfaces and a more nominal approach for classes. With `implements`, TypeScript checks only the shape: method signatures and property names. An abstract class can carry access modifiers, constructors, and concrete methods that `implements` would silently ignore. Using `extends` gives you the full chain: constructor inheritance, concrete method inheritance, and compile-time checks on abstract members. If you could `implements` an abstract class, you could skip all the shared logic it provides, which defeats the purpose of having it. ## Examples ### Basic: shape hierarchy ```typescript abstract class Shape { // Concrete: every shape can describe itself using the same format describe(): string { return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`; } // Abstract: each shape calculates differently abstract area(): number; abstract perimeter(): number; } class Circle extends Shape { constructor(private radius: number) { super(); } area(): number { return Math.PI * this.radius ** 2; } perimeter(): number { return 2 * Math.PI * this.radius; } } class Rectangle extends Shape { constructor(private width: number, private height: number) { super(); } area(): number { return this.width * this.height; } perimeter(): number { return 2 * (this.width + this.height); } } const circle = new Circle(5); circle.describe(); // Area: 78.54, Perimeter: 31.42 const rect = new Rectangle(4, 6); rect.describe(); // Area: 24.00, Perimeter: 20.00 ``` `describe()` is written once and works for every shape. Each subclass only fills in the math it owns. ### Intermediate: API route handler base (NestJS-inspired) ```typescript interface Request { id?: number; body?: unknown; } interface ResponseData { success: boolean; data?: unknown; error?: string; } abstract class ValidatedRoute { // Shared: validation logic reused by every endpoint protected validate(req: Request): boolean { return typeof req.id === 'number' && req.id > 0; } // Abstract: each route defines its own handling abstract handle(req: Request): ResponseData; } class UserRoute extends ValidatedRoute { handle(req: Request): ResponseData { if (!this.validate(req)) { return { success: false, error: 'Invalid ID' }; } return { success: true, data: { id: req.id } }; } } const route = new UserRoute(); route.handle({ id: 1 }); // { success: true, data: { id: 1 } } route.handle({ id: -1 }); // { success: false, error: 'Invalid ID' } ``` This pattern shows up in Express and NestJS middleware. Validation logic lives once in the base. Each route class only defines what happens after validation passes. ### Advanced: repository pattern with generics ```typescript abstract class BaseRepository<T extends { id: string }> { protected abstract collection: string; // Concrete: shared fetch logic reused by all repositories async findById(id: string): Promise<T | null> { const response = await fetch(`/api/${this.collection}/${id}`); if (!response.ok) return null; return response.json(); } // Abstract: each repo defines its own write operations abstract save(entity: T): Promise<T>; abstract delete(id: string): Promise<void>; } interface User { id: string; name: string; } class UserRepository extends BaseRepository<User> { protected collection = 'users'; async save(user: User): Promise<User> { const response = await fetch(`/api/${this.collection}`, { method: 'POST', body: JSON.stringify(user), }); return response.json(); } async delete(id: string): Promise<void> { await fetch(`/api/${this.collection}/${id}`, { method: 'DELETE' }); } } ``` The generic constraint `T extends { id: string }` guarantees every entity has an `id`. `findById` works for every repository that extends this base. Only the write operations differ per entity type.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.