13 Abstract Classes vs Interfaces vs Type Aliases in TS
Coming from Dart, this is a crucial distinction! Dart only has abstract classes (no interface
keyword), but TypeScript has all three with different purposes.
13.1 Overview Comparison
┌─────────────────┬────────────────┬─────────────────┬──────────────────┐
│ │ Abstract Class │ Interface │ Type Alias │
├─────────────────┼────────────────┼─────────────────┼──────────────────┤
│ Implementation │ ✅ │ ❌ │ ❌ │
│ Constructor │ ✅ │ ❌ │ ❌ │
│ Instance │ ❌ (partial) │ ❌ │ ❌ │
│ Multiple │ ❌ │ ✅ │ ✅ │
│ Extend/Merge │ extends │ extends/merge │ intersection │
│ Runtime │ ✅ │ ❌ │ ❌ │
└─────────────────┴────────────────┴─────────────────┴──────────────────┘
13.2 1. Abstract Classes
What: Blueprint classes with partial implementation that can’t be instantiated directly.
Key Features: - Can contain implementation details - Can have constructors - Exist at runtime (they compile to JavaScript) - Single inheritance only - Can have access modifiers (public, private, protected)
// Abstract class with implementation
abstract class Vehicle {
// Can have properties with values
protected fuel: number = 100;
// Can have constructor
constructor(public brand: string) {}
// Can have implemented methods
refuel(): void {
this.fuel = 100;
console.log("Refueled!");
}
// Abstract methods (must be implemented)
abstract startEngine(): void;
abstract calculateRange(): number;
}
class Car extends Vehicle {
constructor(brand: string, private mpg: number) {
super(brand);
}
startEngine(): void {
console.log(`${this.brand} engine started`);
}
calculateRange(): number {
return this.fuel * this.mpg;
}
}
// const vehicle = new Vehicle("Generic"); // ❌ Error! Can't instantiate
const myCar = new Car("Toyota", 30); // ✅ OK
13.3 2. Interfaces
What: Pure contract/shape definition with no implementation.
Key Features: - Only defines structure (no implementation) - Can be implemented by multiple classes - Can extend multiple interfaces - Don’t exist at runtime (completely removed after compilation) - Can be merged (declaration merging)
// Interface - pure structure
interface Drivable {
: number;
speeddrive(): void;
}
interface Flyable {
: number;
altitudefly(): void;
}
interface GPSEnabled {
: { lat: number; lng: number };
coordinatesgetLocation(): void;
}
// Class can implement multiple interfaces
class FlyingCar implements Drivable, Flyable, GPSEnabled {
= 0;
speed = 0;
altitude = { lat: 0, lng: 0 };
coordinates
drive(): void {
this.speed = 60;
}
fly(): void {
this.altitude = 1000;
}
getLocation(): void {
console.log(`Location: ${this.coordinates.lat}, ${this.coordinates.lng}`);
}
}
// Interface extension (can extend multiple)
interface ElectricVehicle extends Drivable, GPSEnabled {
: number;
batteryLevelcharge(): void;
}
// Declaration merging (unique to interfaces!)
interface User {
: string;
name
}
interface User {
: number; // Merged with above
age
}
// Now User has both name and age
const user: User = { name: "Alice", age: 30 };
13.4 3. Type Aliases
What: Creates a new name for any type (primitives, unions, intersections, objects, etc.)
Key Features: - Most flexible - can represent any type - Can create union and intersection types - Can’t be merged (no declaration merging) - Great for complex type compositions - Don’t exist at runtime
// Type alias for object (similar to interface)
type Point = {
: number;
x: number;
y;
}
// Union types (can't do with interfaces!)
type Status = "pending" | "approved" | "rejected";
type ID = string | number;
// Intersection types
type Timestamped = {
: Date;
createdAt: Date;
updatedAt;
}
type Person = {
: string;
name: number;
age;
}
type TimestampedPerson = Person & Timestamped;
// Complex type compositions
type Response<T> =
| { success: true; data: T }
| { success: false; error: string };
// Tuple types
type Coordinate = [number, number, number?]; // x, y, z(optional)
// Function types
type Callback<T> = (data: T) => void;
type Predicate<T> = (item: T) => boolean;
// Utility type combinations
type ReadonlyPartial<T> = Readonly<Partial<T>>;
// Conditional types (advanced)
type IsArray<T> = T extends any[] ? true : false;
13.5 When to Use Each?
13.5.1 Use Abstract Classes when:
// 1. You need shared implementation
abstract class BaseRepository<T> {
protected items: T[] = [];
// Shared implementation
findAll(): T[] {
return this.items;
}
count(): number {
return this.items.length;
}
// Force subclasses to implement
abstract save(item: T): void;
abstract delete(id: string): void;
}
// 2. You need constructor logic
abstract class BaseService {
protected logger: Logger;
constructor(protected apiUrl: string) {
this.logger = new Logger(this.constructor.name);
}
}
// 3. You want "is-a" relationship with shared behavior
abstract class Animal {
constructor(protected name: string) {}
sleep(): void {
console.log(`${this.name} is sleeping`);
}
abstract makeSound(): void;
}
13.5.2 Use Interfaces when:
// 1. Defining contracts for classes
interface PaymentProcessor {
processPayment(amount: number): Promise<boolean>;
refund(transactionId: string): Promise<boolean>;
}
class StripeProcessor implements PaymentProcessor {
async processPayment(amount: number): Promise<boolean> {
// Stripe-specific implementation
return true;
}
async refund(transactionId: string): Promise<boolean> {
// Stripe-specific refund
return true;
}
}
// 2. Describing shape of objects
interface ApiResponse {
: number;
status: any;
data: Date;
timestamp
}
// 3. Multiple inheritance scenarios
interface Serializable {
serialize(): string;
}
interface Cacheable {
getCacheKey(): string;
: number;
ttl
}
class User implements Serializable, Cacheable {
constructor(public id: string, public name: string) {}
serialize(): string {
return JSON.stringify({ id: this.id, name: this.name });
}
getCacheKey(): string {
return `user_${this.id}`;
}
= 3600;
ttl }
13.5.3 Use Type Aliases when:
// 1. Creating union types
type Result<T> = T | Error;
type Theme = "light" | "dark" | "auto";
// 2. Complex type manipulation
type DeepPartial<T> = {
in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
[P ;
}
// 3. Function signatures
type EventHandler<T> = (event: T) => void | Promise<void>;
// 4. Tuples and arrays
type RGB = [red: number, green: number, blue: number];
type Matrix = number[][];
// 5. Combining existing types
type UserProfile = User & {
: Preferences;
preferences: Statistics;
stats; }
13.6 Decision Tree
Need to define a type?
│
┌───────────┴───────────┐
│ │
Need Just type
implementation? structure
│ │
YES NO
│ │
Abstract Class │
┌────────┴────────┐
│ │
For class? Complex type?
│ (union/tuple)
│ │
Interface Type Alias
│
┌──────┴──────┐
│ │
Need merge? Need union?
│ │
Interface Type Alias
13.7 Practical Example: Real-world Scenario
Let’s build a notification system to see all three in action:
// 1. Type Alias for union types
type NotificationType = "email" | "sms" | "push" | "in-app";
type Priority = "low" | "medium" | "high" | "urgent";
// 2. Interface for data structure
interface Notification {
: string;
id: NotificationType;
type: Priority;
priority: string;
recipient: string;
subject: string;
body?: Record<string, any>;
metadata
}
// 3. Interface for service contract
interface NotificationSender {
send(notification: Notification): Promise<boolean>;
validateRecipient(recipient: string): boolean;
}
// 4. Abstract class for shared implementation
abstract class BaseNotificationService implements NotificationSender {
protected queue: Notification[] = [];
// Shared implementation
protected log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
protected addToQueue(notification: Notification): void {
this.queue.push(notification);
}
// Must be implemented by subclasses
abstract send(notification: Notification): Promise<boolean>;
abstract validateRecipient(recipient: string): boolean;
}
// 5. Concrete implementations
class EmailService extends BaseNotificationService {
async send(notification: Notification): Promise<boolean> {
this.log(`Sending email to ${notification.recipient}`);
// Email-specific logic
return true;
}
validateRecipient(recipient: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(recipient);
}
}
class SMSService extends BaseNotificationService {
async send(notification: Notification): Promise<boolean> {
this.log(`Sending SMS to ${notification.recipient}`);
// SMS-specific logic
return true;
}
validateRecipient(recipient: string): boolean {
return /^\+?[1-9]\d{1,14}$/.test(recipient);
}
}
// 6. Type alias for complex return type
type SendResult = {
: boolean;
success: Notification;
notification: Date;
timestamp?: string;
error;
}
// Usage
const emailService = new EmailService();
const notification: Notification = {
: "123",
id: "email",
type: "high",
priority: "user@example.com",
recipient: "Alert",
subject: "Important message"
body;
}
.send(notification); emailService
13.8 Quick Reference
Use Case | Best Choice | Why |
---|---|---|
Shared code between classes | Abstract Class | Can include implementation |
Multiple inheritance | Interface | Classes can implement multiple |
Union types ("a" \| "b" ) |
Type Alias | Interfaces can’t do unions |
Object shape for classes | Interface | Cleaner, supports declaration merging |
Function signatures | Type Alias | More flexible syntax |
Complex type manipulation | Type Alias | Supports conditionals, mappings |
API response types | Interface | Clear structure, extensible |
Enum-like values | Type Alias | Union of literal types |
Base class with helpers | Abstract Class | Shared utility methods |
13.9 Pro Tips
- Start with interfaces for object shapes - they’re simpler and more extensible
- Use type aliases for unions, tuples, and complex type operations
- Use abstract classes sparingly - only when you truly need shared implementation
- Prefer composition over inheritance - use interfaces + composition
- Don’t over-abstract - sometimes a simple interface is all you need
The key insight: Interfaces and Type Aliases are purely compile-time (TypeScript only), while Abstract Classes actually exist in the compiled JavaScript. Choose based on whether you need runtime behavior or just type checking!