+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 40 of 72

๐Ÿค” Interface vs Type Alias: When to Use Each

Master the differences between interfaces and type aliases in TypeScript to make the right choice for your code ๐Ÿš€

๐Ÿš€Intermediate
20 min read

Prerequisites

  • Basic understanding of interfaces ๐Ÿ“
  • Knowledge of type aliases โšก
  • TypeScript fundamentals ๐Ÿ’ป

What you'll learn

  • Understand key differences between interfaces and types ๐ŸŽฏ
  • Know when to use interfaces vs type aliases ๐Ÿ—๏ธ
  • Master unique features of each approach ๐Ÿ›ก๏ธ
  • Make informed architectural decisions โœจ

๐ŸŽฏ Introduction

Welcome to the ultimate showdown: Interfaces vs Type Aliases! ๐ŸŽ‰ In this guide, weโ€™ll settle the age-old TypeScript debate and help you understand when to use each approach for maximum effectiveness.

Youโ€™ll discover that interfaces and type aliases are like different tools in your toolbox ๐Ÿ”ง - each has its strengths and ideal use cases. Whether youโ€™re defining object shapes ๐Ÿ“, creating union types ๐Ÿ”€, or building complex type systems ๐Ÿ—๏ธ, understanding these differences is crucial for writing idiomatic TypeScript.

By the end of this tutorial, youโ€™ll confidently choose the right tool for every situation! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding the Basics

๐Ÿค” Whatโ€™s the Difference?

At first glance, interfaces and type aliases seem to do the same thing:

// ๐ŸŽฏ Interface approach
interface UserInterface {
  id: number;
  name: string;
  email: string;
}

// ๐Ÿท๏ธ Type alias approach  
type UserType = {
  id: number;
  name: string;
  email: string;
};

// โœ… Both work the same way!
const user1: UserInterface = { id: 1, name: "Alice", email: "[email protected]" };
const user2: UserType = { id: 2, name: "Bob", email: "[email protected]" };

But under the hood, they have important differences! Letโ€™s explore them.

๐Ÿ’ก Key Differences at a Glance

Hereโ€™s what makes them unique:

Interfaces ๐ŸŽฏ:

  • Can be extended and implemented
  • Support declaration merging
  • Better for object shapes
  • Show up by name in error messages

Type Aliases ๐Ÿท๏ธ:

  • Can represent any type (not just objects)
  • Support union and intersection types
  • Can use utility types more naturally
  • Cannot be reopened or merged

๐Ÿ”ง When to Use Interfaces

๐Ÿ“ Object Shapes and Contracts

Interfaces excel at defining object shapes:

// ๐Ÿข Perfect use case for interfaces - defining contracts
interface Employee {
  id: string;
  name: string;
  department: string;
  salary: number;
  startDate: Date;
}

interface Manager extends Employee {
  teamSize: number;
  directReports: Employee[];
}

// ๐ŸŽฏ Interfaces work great with classes
class CompanyEmployee implements Employee {
  constructor(
    public id: string,
    public name: string,
    public department: string,
    public salary: number,
    public startDate: Date
  ) {}

  getInfo(): string {
    return `${this.name} (${this.id}) - ${this.department}`;
  }
}

// ๐Ÿ“Š API response shapes
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  page: number;
  pageSize: number;
  totalCount: number;
  hasNext: boolean;
  hasPrevious: boolean;
}

// ๐Ÿช E-commerce example
interface Product {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
}

interface CartItem {
  product: Product;
  quantity: number;
}

interface ShoppingCart {
  items: CartItem[];
  customerId: string;
  createdAt: Date;
  
  addItem(product: Product, quantity: number): void;
  removeItem(productId: string): void;
  getTotalPrice(): number;
}

๐Ÿ”„ Declaration Merging

One unique feature of interfaces is declaration merging:

// ๐ŸŽจ Original interface
interface Theme {
  primaryColor: string;
  secondaryColor: string;
}

// ๐ŸŽฏ Later in the code, we can add more properties!
interface Theme {
  fontSize: number;
  fontFamily: string;
}

// โœ… Both declarations are merged!
const myTheme: Theme = {
  primaryColor: '#007bff',
  secondaryColor: '#6c757d',
  fontSize: 16,
  fontFamily: 'Arial'
};

// ๐Ÿ”ง Great for extending third-party types
interface Window {
  myCustomGlobal: {
    version: string;
    config: any;
  };
}

// Now window.myCustomGlobal is type-safe!
window.myCustomGlobal = {
  version: '1.0.0',
  config: { debug: true }
};

// ๐Ÿ“ฆ Module augmentation
declare module 'express' {
  interface Request {
    user?: {
      id: string;
      role: string;
    };
    session?: {
      token: string;
    };
  }
}

๐ŸŽฏ Interface Extension Examples

Interfaces shine when building hierarchies:

// ๐ŸŽฎ Game character hierarchy
interface GameObject {
  id: string;
  position: { x: number; y: number };
  visible: boolean;
}

interface Moveable {
  velocity: { x: number; y: number };
  move(deltaTime: number): void;
}

interface Damageable {
  health: number;
  maxHealth: number;
  takeDamage(amount: number): void;
}

// ๐Ÿฆธ Combine interfaces easily
interface Player extends GameObject, Moveable, Damageable {
  name: string;
  level: number;
  experience: number;
}

interface Enemy extends GameObject, Moveable, Damageable {
  attackPower: number;
  dropLoot(): void;
}

// ๐Ÿฐ Building system
interface Building extends GameObject {
  buildingType: string;
  owner: Player;
  upgradeCost: number;
}

interface DefensiveBuilding extends Building, Damageable {
  range: number;
  damage: number;
  attack(target: Enemy): void;
}

๐Ÿท๏ธ When to Use Type Aliases

๐Ÿ”€ Union and Intersection Types

Type aliases excel at complex type compositions:

// ๐Ÿšฆ Union types - perfect for type aliases
type Status = 'idle' | 'loading' | 'success' | 'error';
type Priority = 'low' | 'medium' | 'high' | 'urgent';

// ๐ŸŽฏ More complex unions
type StringOrNumber = string | number;
type Nullable<T> = T | null;
type Optional<T> = T | undefined;

// ๐Ÿ”„ Result type pattern
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

function fetchUser(id: string): Result<User> {
  try {
    // Fetch logic...
    return { success: true, data: { id: '1', name: 'Alice' } as User };
  } catch (error) {
    return { success: false, error: error as Error };
  }
}

// ๐ŸŽจ Theme variants
type ColorScheme = 'light' | 'dark' | 'auto';
type ThemeColor = 'primary' | 'secondary' | 'success' | 'danger' | 'warning';

type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link';
type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';

// ๐Ÿช E-commerce status types
type OrderStatus = 
  | 'pending'
  | 'processing' 
  | 'shipped'
  | 'delivered'
  | 'cancelled'
  | 'refunded';

type PaymentMethod = 
  | { type: 'credit_card'; last4: string; brand: string }
  | { type: 'paypal'; email: string }
  | { type: 'crypto'; wallet: string; currency: string }
  | { type: 'bank_transfer'; accountNumber: string };

๐Ÿงฎ Complex Type Transformations

Type aliases are better for utility types and transformations:

// ๐Ÿ”ง Utility type compositions
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Required<T> = {
  [P in keyof T]-?: T[P];
};

// ๐ŸŽฏ Custom utility types
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

type Stringify<T> = {
  [P in keyof T]: string;
};

// ๐Ÿ”‘ Extract keys of certain type
type KeysOfType<T, U> = {
  [K in keyof T]: T[K] extends U ? K : never;
}[keyof T];

interface Person {
  name: string;
  age: number;
  email: string;
  isActive: boolean;
}

type StringKeys = KeysOfType<Person, string>; // "name" | "email"
type NumberKeys = KeysOfType<Person, number>; // "age"

// ๐ŸŽจ Deep modifications
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// ๐Ÿ“Š API types with transformations
type ApiEndpoints = {
  users: '/api/users';
  posts: '/api/posts';
  comments: '/api/comments';
};

type ApiClient = {
  [K in keyof ApiEndpoints as `get${Capitalize<string & K>}`]: 
    () => Promise<Response>;
};
// Results in: { getUsers: () => Promise<Response>; ... }

๐ŸŽฏ Function Types and Tuples

Type aliases are cleaner for function signatures and tuples:

// ๐ŸŽฏ Function type aliases
type Validator<T> = (value: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type AsyncOperation<T> = () => Promise<T>;
type EventHandler<T = void> = (event: T) => void;

// ๐Ÿ”„ Callback patterns
type Callback<T> = (error: Error | null, data?: T) => void;
type UnsubscribeFn = () => void;

// ๐Ÿ“ฆ Tuple types
type Coordinates = [number, number];
type RGB = [number, number, number];
type RGBA = [...RGB, number];

type UserTuple = [id: string, name: string, age: number];
type ResponseTuple = [data: any, error: Error | null];

// ๐ŸŽฎ Game examples
type Vector2D = [x: number, y: number];
type Vector3D = [...Vector2D, z: number];
type Matrix3x3 = [
  [number, number, number],
  [number, number, number],
  [number, number, number]
];

// ๐Ÿ”ง Middleware pattern
type Middleware<T> = (
  context: T,
  next: () => Promise<void>
) => Promise<void>;

type Pipeline<T> = {
  use(middleware: Middleware<T>): Pipeline<T>;
  execute(context: T): Promise<void>;
};

// ๐Ÿ“Š Data processing functions
type MapFunction<T, U> = (item: T, index: number, array: T[]) => U;
type FilterFunction<T> = (item: T, index: number, array: T[]) => boolean;
type ReduceFunction<T, U> = (acc: U, item: T, index: number, array: T[]) => U;

๐Ÿ’ก Practical Comparison Examples

๐Ÿ—๏ธ Building a User System

Letโ€™s see both approaches side by side:

// ๐ŸŽฏ INTERFACE APPROACH - Better for object hierarchies
interface IUser {
  id: string;
  username: string;
  email: string;
  createdAt: Date;
}

interface IAdmin extends IUser {
  permissions: string[];
  adminSince: Date;
}

class UserImpl implements IUser {
  constructor(
    public id: string,
    public username: string,
    public email: string,
    public createdAt: Date
  ) {}
}

// ๐Ÿท๏ธ TYPE ALIAS APPROACH - Better for unions and utilities
type UserRole = 'user' | 'admin' | 'moderator' | 'guest';
type UserStatus = 'active' | 'suspended' | 'deleted';

type UserWithRole = {
  id: string;
  username: string;
  email: string;
  role: UserRole;
  status: UserStatus;
};

type AdminUser = UserWithRole & {
  permissions: string[];
  adminSince: Date;
};

// ๐Ÿ”€ Union types for different user states
type AuthState = 
  | { type: 'authenticated'; user: IUser; token: string }
  | { type: 'unauthenticated' }
  | { type: 'loading' };

// ๐ŸŽจ Utility types with type aliases
type PublicUserInfo = Pick<IUser, 'id' | 'username'>;
type UserUpdate = Partial<Omit<IUser, 'id' | 'createdAt'>>;
type UserCredentials = Pick<IUser, 'email'> & { password: string };

๐ŸŽฎ Game Development Example

// ๐ŸŽฏ INTERFACES - For game objects and contracts
interface IGameObject {
  id: string;
  position: { x: number; y: number; z: number };
  rotation: { x: number; y: number; z: number };
  scale: { x: number; y: number; z: number };
}

interface IRenderable {
  mesh: string;
  texture: string;
  render(): void;
}

interface IUpdatable {
  update(deltaTime: number): void;
}

interface ICollidable {
  boundingBox: { min: Vector3D; max: Vector3D };
  onCollision(other: ICollidable): void;
}

// Combine interfaces for game entities
interface IGameEntity extends IGameObject, IRenderable, IUpdatable, ICollidable {
  name: string;
  health: number;
}

// ๐Ÿท๏ธ TYPE ALIASES - For game state and configurations
type GameState = 'menu' | 'playing' | 'paused' | 'gameOver';
type Difficulty = 'easy' | 'normal' | 'hard' | 'nightmare';

type Vector3D = { x: number; y: number; z: number };
type Color = { r: number; g: number; b: number; a?: number };

type InputAction = 
  | { type: 'move'; direction: Vector3D }
  | { type: 'jump' }
  | { type: 'attack'; target?: IGameEntity }
  | { type: 'useItem'; itemId: string };

type GameConfig = {
  difficulty: Difficulty;
  graphics: 'low' | 'medium' | 'high' | 'ultra';
  audio: {
    master: number;
    music: number;
    effects: number;
  };
  controls: Record<string, string>;
};

// Function types for game systems
type UpdateFunction = (deltaTime: number) => void;
type RenderFunction = (renderer: any) => void;
type CollisionHandler = (a: ICollidable, b: ICollidable) => void;

๐ŸŒ API Design Patterns

// ๐ŸŽฏ INTERFACES - For request/response structures
interface ApiRequest {
  endpoint: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: unknown;
}

interface ApiResponse<T> {
  data: T;
  status: number;
  headers: Record<string, string>;
}

interface ApiError {
  message: string;
  code: string;
  details?: unknown;
}

// Service contracts
interface IAuthService {
  login(credentials: UserCredentials): Promise<AuthToken>;
  logout(): Promise<void>;
  refresh(token: string): Promise<AuthToken>;
}

interface IDataService<T> {
  getAll(): Promise<T[]>;
  getById(id: string): Promise<T>;
  create(data: Omit<T, 'id'>): Promise<T>;
  update(id: string, data: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

// ๐Ÿท๏ธ TYPE ALIASES - For API configurations and utilities
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiVersion = 'v1' | 'v2' | 'v3';

type EndpointConfig = {
  path: string;
  method: HttpMethod;
  auth: boolean;
  cache?: number;
  rateLimit?: {
    requests: number;
    window: number;
  };
};

type ApiResult<T> = 
  | { ok: true; data: T }
  | { ok: false; error: ApiError };

type RequestInterceptor = (config: ApiRequest) => ApiRequest | Promise<ApiRequest>;
type ResponseInterceptor = <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;

// Complex type for API client configuration
type ApiClientConfig = {
  baseURL: string;
  version: ApiVersion;
  timeout: number;
  headers: Record<string, string>;
  interceptors: {
    request: RequestInterceptor[];
    response: ResponseInterceptor[];
  };
  retry: {
    attempts: number;
    delay: number;
    statusCodes: number[];
  };
};

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Conditional Types with Type Aliases

Type aliases are essential for conditional types:

// ๐ŸŽฏ Conditional type examples
type IsString<T> = T extends string ? true : false;
type IsArray<T> = T extends any[] ? true : false;

// ๐Ÿ”ง Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type El1 = ArrayElement<string[]>; // string
type El2 = ArrayElement<number[]>; // number

// ๐ŸŽจ Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => Promise<number>>; // Promise<number>

// ๐Ÿ“ฆ Unwrap promises
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type U1 = UnwrapPromise<Promise<string>>; // string
type U2 = UnwrapPromise<string>; // string

// ๐Ÿ”€ Discriminated union helpers
type ExtractByType<T, U> = T extends { type: U } ? T : never;

type Events = 
  | { type: 'click'; x: number; y: number }
  | { type: 'change'; value: string }
  | { type: 'submit'; data: object };

type ClickEvent = ExtractByType<Events, 'click'>;
// { type: 'click'; x: number; y: number }

// ๐ŸŽฏ Deep property access
type DeepKeyOf<T> = T extends object
  ? {
      [K in keyof T]: K extends string
        ? T[K] extends object
          ? K | `${K}.${DeepKeyOf<T[K]>}`
          : K
        : never;
    }[keyof T]
  : never;

interface NestedObject {
  user: {
    name: string;
    address: {
      street: string;
      city: string;
    };
  };
}

type Keys = DeepKeyOf<NestedObject>;
// "user" | "user.name" | "user.address" | "user.address.street" | "user.address.city"

๐Ÿ—๏ธ Template Literal Types

Type aliases work great with template literals:

// ๐ŸŽจ CSS unit types
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

const width: CSSValue = '100px'; // โœ…
const height: CSSValue = '50vh'; // โœ…
// const invalid: CSSValue = '100'; // โŒ Error!

// ๐ŸŽฏ Event handler types
type EventType = 'click' | 'change' | 'submit' | 'hover';
type EventHandler = `on${Capitalize<EventType>}`;
// "onClick" | "onChange" | "onSubmit" | "onHover"

// ๐Ÿ”ง API route builder
type HTTPMethod = 'get' | 'post' | 'put' | 'delete';
type APIRoute<T extends string> = `/${T}` | `/${T}/:id`;
type APIMethod<M extends HTTPMethod, R extends string> = `${Uppercase<M>} ${APIRoute<R>}`;

type UserRoutes = APIMethod<HTTPMethod, 'users'>;
// "GET /users" | "GET /users/:id" | "POST /users" | "POST /users/:id" | ...

// ๐ŸŽฎ Game command system
type Direction = 'north' | 'south' | 'east' | 'west';
type Action = 'move' | 'look' | 'go';
type GameCommand = `${Action} ${Direction}`;

const cmd1: GameCommand = 'move north'; // โœ…
const cmd2: GameCommand = 'look west'; // โœ…
// const cmd3: GameCommand = 'jump south'; // โŒ Error!

// ๐Ÿ“Š Dynamic property names
type PropEventSource<T> = {
  on<K extends string & keyof T>(
    eventName: `${K}Changed`,
    callback: (newValue: T[K]) => void
  ): void;
};

interface User {
  name: string;
  age: number;
}

declare const user: PropEventSource<User>;
user.on('nameChanged', (newName) => console.log(newName)); // โœ…
user.on('ageChanged', (newAge) => console.log(newAge)); // โœ…
// user.on('emailChanged', () => {}); // โŒ Error!

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Trying to Extend Type Aliases

// โŒ Wrong - can't extend type aliases directly
type Animal = {
  name: string;
  age: number;
};

// This doesn't work!
// type Dog extends Animal {
//   breed: string;
// }

// โœ… Solution 1: Use intersection
type Dog = Animal & {
  breed: string;
};

// โœ… Solution 2: Use interfaces
interface IAnimal {
  name: string;
  age: number;
}

interface IDog extends IAnimal {
  breed: string;
}

๐Ÿคฏ Pitfall 2: Declaration Merging with Types

// โŒ Wrong - type aliases can't be merged
type Config = {
  apiUrl: string;
};

// Error: Duplicate identifier 'Config'
// type Config = {
//   timeout: number;
// };

// โœ… Correct - interfaces can be merged
interface IConfig {
  apiUrl: string;
}

interface IConfig {
  timeout: number; // This gets merged!
}

const config: IConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
};

๐Ÿ”„ Pitfall 3: Implementing Type Aliases

// โŒ Wrong - can't implement type aliases
type Flyable = {
  fly(): void;
};

// Error: 'Flyable' only refers to a type
// class Bird implements Flyable {
//   fly() { console.log('Flying!'); }
// }

// โœ… Correct - use interfaces for implementation
interface IFlyable {
  fly(): void;
}

class Bird implements IFlyable {
  fly() { 
    console.log('๐Ÿฆ… Flying high!'); 
  }
}

๐Ÿ› ๏ธ Best Practices

๐ŸŽฏ Guidelines for Choosing

Use Interfaces When:

  1. ๐Ÿ“ Defining object shapes
  2. ๐Ÿ—๏ธ Building class hierarchies
  3. ๐Ÿ“œ Creating public API contracts
  4. ๐Ÿ”„ You might need declaration merging
  5. ๐ŸŽจ Working with OOP patterns

Use Type Aliases When:

  1. ๐Ÿ”€ Creating union or intersection types
  2. ๐ŸŽฏ Defining function signatures
  3. ๐Ÿ“ฆ Working with tuples
  4. ๐Ÿงฎ Building utility types
  5. ๐Ÿ”ง Creating type transformations

๐Ÿ“ Naming Conventions

// ๐ŸŽฏ Interface naming - use "I" prefix or descriptive names
interface IUser { } // Traditional
interface UserInterface { } // Descriptive
interface User { } // Clean (preferred by many)

// ๐Ÿท๏ธ Type alias naming - descriptive of what it represents
type UserId = string;
type UserRole = 'admin' | 'user';
type UserStatus = 'active' | 'inactive';
type GetUserFunction = (id: string) => Promise<User>;

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Flexible Event System

Create an event system that showcases when to use interfaces vs type aliases:

๐Ÿ“‹ Requirements:

  • โœ… Event base structure using interface
  • ๐Ÿ”€ Event types using type unions
  • ๐ŸŽฏ Event handlers using type aliases
  • ๐Ÿ“Š Event emitter with proper typing
  • ๐Ÿ”ง Utility types for event filtering

๐Ÿš€ Bonus Points:

  • Add event namespacing
  • Implement event bubbling
  • Create typed event maps

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ INTERFACES - For extensible structures
interface IEvent {
  id: string;
  timestamp: Date;
  source: string;
  bubbles: boolean;
  cancelable: boolean;
}

interface IEventTarget {
  addEventListener<E extends IEvent>(
    type: string,
    listener: EventListener<E>
  ): void;
  removeEventListener<E extends IEvent>(
    type: string,
    listener: EventListener<E>
  ): void;
  dispatchEvent(event: IEvent): boolean;
}

// Extend for specific event types
interface IMouseEvent extends IEvent {
  x: number;
  y: number;
  button: number;
}

interface IKeyboardEvent extends IEvent {
  key: string;
  code: string;
  ctrlKey: boolean;
  shiftKey: boolean;
  altKey: boolean;
}

interface ICustomEvent<T = any> extends IEvent {
  detail: T;
}

// ๐Ÿท๏ธ TYPE ALIASES - For unions, functions, and utilities
type EventType = 'click' | 'dblclick' | 'keydown' | 'keyup' | 'custom';
type EventPhase = 'capture' | 'target' | 'bubble';

type EventListener<E extends IEvent> = (event: E) => void;
type EventFilter<E extends IEvent> = (event: E) => boolean;

// Complex event map type
type EventMap = {
  click: IMouseEvent;
  dblclick: IMouseEvent;
  keydown: IKeyboardEvent;
  keyup: IKeyboardEvent;
  custom: ICustomEvent;
};

// Utility types for events
type EventNames = keyof EventMap;
type EventOfType<T extends EventNames> = EventMap[T];

// Advanced handler types
type TypedEventListener<K extends EventNames> = (event: EventMap[K]) => void;
type EventListenerOptions = {
  once?: boolean;
  passive?: boolean;
  capture?: boolean;
};

// ๐ŸŽฏ Event Emitter Implementation
class TypedEventEmitter implements IEventTarget {
  private listeners: Map<string, Set<EventListener<any>>> = new Map();
  private eventHistory: IEvent[] = [];

  addEventListener<K extends EventNames>(
    type: K,
    listener: TypedEventListener<K>,
    options?: EventListenerOptions
  ): void {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, new Set());
    }

    const wrappedListener = options?.once
      ? (event: EventMap[K]) => {
          listener(event);
          this.removeEventListener(type, wrappedListener);
        }
      : listener;

    this.listeners.get(type)!.add(wrappedListener);
    console.log(`๐ŸŽง Added ${type} listener`);
  }

  removeEventListener<K extends EventNames>(
    type: K,
    listener: TypedEventListener<K>
  ): void {
    const listeners = this.listeners.get(type);
    if (listeners) {
      listeners.delete(listener);
      console.log(`๐Ÿ”‡ Removed ${type} listener`);
    }
  }

  dispatchEvent(event: IEvent): boolean {
    this.eventHistory.push(event);
    const listeners = this.listeners.get(event.source);
    
    if (!listeners || listeners.size === 0) {
      console.log(`๐Ÿ“ข No listeners for ${event.source}`);
      return true;
    }

    let propagate = true;
    listeners.forEach(listener => {
      try {
        listener(event);
      } catch (error) {
        console.error(`โŒ Error in ${event.source} listener:`, error);
      }
    });

    return propagate;
  }

  // Utility methods using type aliases
  on<K extends EventNames>(
    type: K,
    listener: TypedEventListener<K>
  ): () => void {
    this.addEventListener(type, listener);
    // Return unsubscribe function
    return () => this.removeEventListener(type, listener);
  }

  once<K extends EventNames>(
    type: K,
    listener: TypedEventListener<K>
  ): void {
    this.addEventListener(type, listener, { once: true });
  }

  emit<K extends EventNames>(
    type: K,
    eventData: Omit<EventMap[K], keyof IEvent>
  ): void {
    const event = {
      ...eventData,
      id: `evt_${Date.now()}`,
      timestamp: new Date(),
      source: type,
      bubbles: true,
      cancelable: true
    } as EventMap[K];

    this.dispatchEvent(event);
  }

  // Get events by filter
  getEventHistory<E extends IEvent>(
    filter?: EventFilter<E>
  ): E[] {
    if (!filter) return this.eventHistory as E[];
    return this.eventHistory.filter(filter) as E[];
  }
}

// ๐Ÿ”ง Event builder using both approaches
interface IEventBuilder<E extends IEvent> {
  setSource(source: string): this;
  setBubbles(bubbles: boolean): this;
  setCancelable(cancelable: boolean): this;
  build(): E;
}

type EventBuilderConfig<E extends IEvent> = {
  [K in keyof E]?: E[K];
};

class MouseEventBuilder implements IEventBuilder<IMouseEvent> {
  private config: EventBuilderConfig<IMouseEvent> = {
    id: `evt_${Date.now()}`,
    timestamp: new Date(),
    source: 'unknown',
    bubbles: true,
    cancelable: true,
    x: 0,
    y: 0,
    button: 0
  };

  setSource(source: string): this {
    this.config.source = source;
    return this;
  }

  setBubbles(bubbles: boolean): this {
    this.config.bubbles = bubbles;
    return this;
  }

  setCancelable(cancelable: boolean): this {
    this.config.cancelable = cancelable;
    return this;
  }

  setPosition(x: number, y: number): this {
    this.config.x = x;
    this.config.y = y;
    return this;
  }

  setButton(button: number): this {
    this.config.button = button;
    return this;
  }

  build(): IMouseEvent {
    return this.config as IMouseEvent;
  }
}

// ๐ŸŽฎ Demo the event system
const emitter = new TypedEventEmitter();

// Type-safe event listeners
const unsubscribeClick = emitter.on('click', (event) => {
  console.log(`๐Ÿ–ฑ๏ธ Click at (${event.x}, ${event.y})`);
});

emitter.on('keydown', (event) => {
  console.log(`โŒจ๏ธ Key pressed: ${event.key}`);
  if (event.ctrlKey && event.key === 's') {
    console.log('๐Ÿ’พ Save shortcut detected!');
  }
});

emitter.once('custom', (event) => {
  console.log(`๐ŸŽฏ Custom event (once): ${JSON.stringify(event.detail)}`);
});

// Emit events
console.log('\n๐Ÿš€ Emitting events...\n');

emitter.emit('click', { x: 100, y: 200, button: 0 });
emitter.emit('keydown', { 
  key: 's', 
  code: 'KeyS', 
  ctrlKey: true, 
  shiftKey: false, 
  altKey: false 
});
emitter.emit('custom', { detail: { message: 'Hello!' } });
emitter.emit('custom', { detail: { message: 'This won\'t show!' } });

// Use builder pattern
const clickEvent = new MouseEventBuilder()
  .setSource('click')
  .setPosition(250, 350)
  .setButton(2)
  .build();

emitter.dispatchEvent(clickEvent);

// Event history with filtering
console.log('\n๐Ÿ“œ Event History:');
const mouseEvents = emitter.getEventHistory<IMouseEvent>(
  event => 'x' in event && 'y' in event
);
console.log(`Found ${mouseEvents.length} mouse events`);

// Cleanup
unsubscribeClick();
console.log('\n๐Ÿงน Cleaned up click listener');
emitter.emit('click', { x: 300, y: 400, button: 0 }); // Won't trigger

๐ŸŽ“ Key Takeaways

You now understand when to use interfaces vs type aliases! Hereโ€™s what youโ€™ve learned:

  • โœ… Interfaces are best for object shapes and OOP ๐ŸŽฏ
  • โœ… Type aliases excel at unions and complex types ๐Ÿท๏ธ
  • โœ… Declaration merging only works with interfaces ๐Ÿ”„
  • โœ… Both can often achieve the same goal ๐Ÿค
  • โœ… Choose based on your specific use case ๐ŸŽจ

Remember: Itโ€™s not about which is โ€œbetterโ€ - itโ€™s about using the right tool for the job! ๐Ÿ”ง

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the interface vs type alias decision!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the event system exercise above
  2. ๐Ÿ—๏ธ Refactor existing code to use the appropriate approach
  3. ๐Ÿ“š Move on to our next tutorial: Extending Interfaces: Building Complex Types
  4. ๐ŸŒŸ Create your own style guide for your team!

Remember: The best TypeScript code uses both interfaces and type aliases appropriately. Keep learning, keep building! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ