Abstract classes provide base implementations that cannot be instantiated directly. They define contracts that derived classes must implement.
Basic Syntax#
1// Abstract class - cannot be instantiated
2abstract class Animal {
3 name: string;
4
5 constructor(name: string) {
6 this.name = name;
7 }
8
9 // Abstract method - must be implemented
10 abstract makeSound(): string;
11
12 // Concrete method - shared implementation
13 move(distance: number): void {
14 console.log(`${this.name} moved ${distance} meters`);
15 }
16}
17
18// Concrete class
19class Dog extends Animal {
20 makeSound(): string {
21 return 'Woof!';
22 }
23}
24
25class Cat extends Animal {
26 makeSound(): string {
27 return 'Meow!';
28 }
29}
30
31// const animal = new Animal('Generic'); // Error: Cannot create instance of abstract class
32const dog = new Dog('Rex');
33console.log(dog.makeSound()); // 'Woof!'
34dog.move(10); // 'Rex moved 10 meters'Abstract Properties#
1abstract class Shape {
2 // Abstract property
3 abstract readonly area: number;
4 abstract readonly perimeter: number;
5
6 // Concrete property
7 color: string = 'black';
8
9 abstract describe(): string;
10}
11
12class Rectangle extends Shape {
13 constructor(
14 private width: number,
15 private height: number
16 ) {
17 super();
18 }
19
20 get area(): number {
21 return this.width * this.height;
22 }
23
24 get perimeter(): number {
25 return 2 * (this.width + this.height);
26 }
27
28 describe(): string {
29 return `Rectangle: ${this.width}x${this.height}`;
30 }
31}
32
33class Circle extends Shape {
34 constructor(private radius: number) {
35 super();
36 }
37
38 get area(): number {
39 return Math.PI * this.radius ** 2;
40 }
41
42 get perimeter(): number {
43 return 2 * Math.PI * this.radius;
44 }
45
46 describe(): string {
47 return `Circle: radius ${this.radius}`;
48 }
49}Template Method Pattern#
1abstract class DataProcessor {
2 // Template method - defines the algorithm
3 process(data: string): string {
4 const validated = this.validate(data);
5 const parsed = this.parse(validated);
6 const transformed = this.transform(parsed);
7 return this.format(transformed);
8 }
9
10 // Abstract methods - steps to be implemented
11 protected abstract validate(data: string): string;
12 protected abstract parse(data: string): object;
13 protected abstract transform(data: object): object;
14
15 // Concrete method with default implementation
16 protected format(data: object): string {
17 return JSON.stringify(data);
18 }
19}
20
21class JSONProcessor extends DataProcessor {
22 protected validate(data: string): string {
23 if (!data.trim()) throw new Error('Empty data');
24 return data;
25 }
26
27 protected parse(data: string): object {
28 return JSON.parse(data);
29 }
30
31 protected transform(data: object): object {
32 return { ...data, processed: true, timestamp: Date.now() };
33 }
34}
35
36class CSVProcessor extends DataProcessor {
37 protected validate(data: string): string {
38 if (!data.includes(',')) throw new Error('Invalid CSV');
39 return data;
40 }
41
42 protected parse(data: string): object {
43 const lines = data.split('\n');
44 const headers = lines[0].split(',');
45 return lines.slice(1).map((line) => {
46 const values = line.split(',');
47 return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
48 });
49 }
50
51 protected transform(data: object): object {
52 return { rows: data, count: (data as any[]).length };
53 }
54}Factory Pattern#
1abstract class Vehicle {
2 abstract type: string;
3 abstract wheels: number;
4
5 abstract start(): void;
6 abstract stop(): void;
7
8 describe(): string {
9 return `${this.type} with ${this.wheels} wheels`;
10 }
11}
12
13class Car extends Vehicle {
14 type = 'Car';
15 wheels = 4;
16
17 start(): void {
18 console.log('Car engine started');
19 }
20
21 stop(): void {
22 console.log('Car engine stopped');
23 }
24}
25
26class Motorcycle extends Vehicle {
27 type = 'Motorcycle';
28 wheels = 2;
29
30 start(): void {
31 console.log('Motorcycle engine started');
32 }
33
34 stop(): void {
35 console.log('Motorcycle engine stopped');
36 }
37}
38
39// Factory
40abstract class VehicleFactory {
41 abstract createVehicle(): Vehicle;
42
43 orderVehicle(): Vehicle {
44 const vehicle = this.createVehicle();
45 console.log(`Creating ${vehicle.describe()}`);
46 vehicle.start();
47 return vehicle;
48 }
49}
50
51class CarFactory extends VehicleFactory {
52 createVehicle(): Vehicle {
53 return new Car();
54 }
55}
56
57class MotorcycleFactory extends VehicleFactory {
58 createVehicle(): Vehicle {
59 return new Motorcycle();
60 }
61}Abstract with Generics#
1abstract class Repository<T extends { id: string }> {
2 protected items: Map<string, T> = new Map();
3
4 abstract validate(item: T): boolean;
5
6 save(item: T): void {
7 if (!this.validate(item)) {
8 throw new Error('Validation failed');
9 }
10 this.items.set(item.id, item);
11 }
12
13 findById(id: string): T | undefined {
14 return this.items.get(id);
15 }
16
17 findAll(): T[] {
18 return Array.from(this.items.values());
19 }
20
21 delete(id: string): boolean {
22 return this.items.delete(id);
23 }
24}
25
26interface User {
27 id: string;
28 name: string;
29 email: string;
30}
31
32class UserRepository extends Repository<User> {
33 validate(user: User): boolean {
34 return (
35 user.name.length > 0 &&
36 user.email.includes('@')
37 );
38 }
39
40 findByEmail(email: string): User | undefined {
41 return this.findAll().find((u) => u.email === email);
42 }
43}Abstract Static Members#
1// Abstract classes can have static members
2abstract class Logger {
3 static instances: Logger[] = [];
4
5 constructor() {
6 Logger.instances.push(this);
7 }
8
9 abstract log(message: string): void;
10
11 // Static factory method
12 static create(type: 'console' | 'file'): Logger {
13 switch (type) {
14 case 'console':
15 return new ConsoleLogger();
16 case 'file':
17 return new FileLogger();
18 }
19 }
20}
21
22class ConsoleLogger extends Logger {
23 log(message: string): void {
24 console.log(`[Console] ${message}`);
25 }
26}
27
28class FileLogger extends Logger {
29 log(message: string): void {
30 console.log(`[File] ${message}`);
31 }
32}
33
34const logger = Logger.create('console');
35logger.log('Hello');Hook Methods#
1abstract class Component {
2 private mounted = false;
3
4 // Lifecycle hooks - can be overridden
5 protected onBeforeMount(): void {}
6 protected onMounted(): void {}
7 protected onBeforeUnmount(): void {}
8 protected onUnmounted(): void {}
9
10 // Abstract method - must be implemented
11 abstract render(): string;
12
13 mount(): void {
14 this.onBeforeMount();
15 const html = this.render();
16 // Mount to DOM
17 this.mounted = true;
18 this.onMounted();
19 }
20
21 unmount(): void {
22 if (!this.mounted) return;
23 this.onBeforeUnmount();
24 // Remove from DOM
25 this.mounted = false;
26 this.onUnmounted();
27 }
28}
29
30class UserCard extends Component {
31 constructor(private user: { name: string }) {
32 super();
33 }
34
35 protected onMounted(): void {
36 console.log('UserCard mounted');
37 }
38
39 protected onUnmounted(): void {
40 console.log('UserCard unmounted');
41 }
42
43 render(): string {
44 return `<div class="user-card">${this.user.name}</div>`;
45 }
46}Combining with Interfaces#
1interface Serializable {
2 serialize(): string;
3 deserialize(data: string): void;
4}
5
6interface Comparable<T> {
7 compareTo(other: T): number;
8}
9
10abstract class Entity implements Serializable, Comparable<Entity> {
11 abstract id: string;
12 abstract name: string;
13
14 // From Serializable
15 serialize(): string {
16 return JSON.stringify({ id: this.id, name: this.name });
17 }
18
19 abstract deserialize(data: string): void;
20
21 // From Comparable
22 compareTo(other: Entity): number {
23 return this.name.localeCompare(other.name);
24 }
25}
26
27class Product extends Entity {
28 constructor(
29 public id: string,
30 public name: string,
31 public price: number
32 ) {
33 super();
34 }
35
36 deserialize(data: string): void {
37 const parsed = JSON.parse(data);
38 this.id = parsed.id;
39 this.name = parsed.name;
40 this.price = parsed.price;
41 }
42
43 serialize(): string {
44 return JSON.stringify({
45 id: this.id,
46 name: this.name,
47 price: this.price,
48 });
49 }
50}Protected Abstract#
1abstract class HttpClient {
2 protected abstract baseUrl: string;
3 protected abstract headers: Record<string, string>;
4
5 protected abstract handleError(error: Error): void;
6
7 async get<T>(path: string): Promise<T> {
8 try {
9 const response = await fetch(`${this.baseUrl}${path}`, {
10 headers: this.headers,
11 });
12 return response.json();
13 } catch (error) {
14 this.handleError(error as Error);
15 throw error;
16 }
17 }
18
19 async post<T>(path: string, data: unknown): Promise<T> {
20 try {
21 const response = await fetch(`${this.baseUrl}${path}`, {
22 method: 'POST',
23 headers: this.headers,
24 body: JSON.stringify(data),
25 });
26 return response.json();
27 } catch (error) {
28 this.handleError(error as Error);
29 throw error;
30 }
31 }
32}
33
34class ApiClient extends HttpClient {
35 protected baseUrl = 'https://api.example.com';
36 protected headers = {
37 'Content-Type': 'application/json',
38 'Authorization': `Bearer ${process.env.API_TOKEN}`,
39 };
40
41 protected handleError(error: Error): void {
42 console.error('API Error:', error.message);
43 // Send to error tracking service
44 }
45}Best Practices#
When to Use:
✓ Shared code with enforced contracts
✓ Template method pattern
✓ Factory patterns
✓ Framework base classes
Design:
✓ Keep abstract methods focused
✓ Provide sensible defaults
✓ Document expected behavior
✓ Use protected for internals
Composition:
✓ Prefer composition over inheritance
✓ Combine with interfaces
✓ Use generics for flexibility
✓ Keep hierarchies shallow
Avoid:
✗ Deep inheritance hierarchies
✗ Too many abstract methods
✗ Abstract classes without shared code
✗ Breaking Liskov substitution
Conclusion#
Abstract classes combine contracts (like interfaces) with implementation sharing. Use them when you need to share code between related classes while enforcing certain methods to be implemented. They're ideal for template method patterns, factories, and framework base classes. Keep inheritance hierarchies shallow and prefer composition when classes aren't truly related through an "is-a" relationship.