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:
- ๐ Defining object shapes
- ๐๏ธ Building class hierarchies
- ๐ Creating public API contracts
- ๐ You might need declaration merging
- ๐จ Working with OOP patterns
Use Type Aliases When:
- ๐ Creating union or intersection types
- ๐ฏ Defining function signatures
- ๐ฆ Working with tuples
- ๐งฎ Building utility types
- ๐ง 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:
- ๐ป Practice with the event system exercise above
- ๐๏ธ Refactor existing code to use the appropriate approach
- ๐ Move on to our next tutorial: Extending Interfaces: Building Complex Types
- ๐ 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! ๐๐โจ