Prerequisites
- โ Basic understanding of TypeScript classes
- ๐ Familiarity with object-oriented programming concepts
- ๐ง Knowledge of class properties and methods
What you'll learn
- ๐ฏ Understand the fundamental difference between instance and static members
- ๐ ๏ธ Learn when to use instance vs static members effectively
- ๐ก Master memory and performance implications
- ๐ Apply best practices for OOP design in TypeScript
๐ฏ Introduction
Welcome to the world of instance and static members in TypeScript! ๐ Have you ever wondered why some properties belong to the class itself while others belong to each object? Today, weโll unravel this mystery and help you become a master of object-oriented design! ๐๏ธ
Think of a class as a cookie cutter ๐ช and instances as the actual cookies. Static members belong to the cookie cutter itself, while instance members belong to each individual cookie. Letโs explore this delicious concept! ๐
๐ Understanding Instance vs Static Members
๐ญ The Two Faces of Class Members
In TypeScript, class members can exist in two forms:
- Instance Members ๐โโ๏ธ: Belong to each individual object created from the class
- Static Members ๐๏ธ: Belong to the class itself, shared across all instances
class GameCharacter {
// Instance property - each character has their own health ๐
health: number = 100;
// Static property - shared across all characters ๐
static totalCharactersCreated: number = 0;
constructor(public name: string) {
// Increment the static counter when creating a new character ๐
GameCharacter.totalCharactersCreated++;
}
// Instance method - affects individual character ๐ฎ
takeDamage(amount: number): void {
this.health -= amount;
console.log(`${this.name} took ${amount} damage! Health: ${this.health} ๐`);
}
// Static method - operates at class level ๐๏ธ
static getTotalCharacters(): number {
return GameCharacter.totalCharactersCreated;
}
}
// Creating instances ๐จ
const hero = new GameCharacter("Hero");
const villain = new GameCharacter("Villain");
// Instance members are unique to each object ๐ฏ
hero.takeDamage(20); // Hero took 20 damage! Health: 80 ๐
villain.takeDamage(10); // Villain took 10 damage! Health: 90 ๐
// Static members are shared ๐ค
console.log(GameCharacter.getTotalCharacters()); // 2
console.log(GameCharacter.totalCharactersCreated); // 2
๐ Key Differences at a Glance
Aspect | Instance Members | Static Members |
---|---|---|
Belongs to | Each object ๐ฏ | The class itself ๐๏ธ |
Access via | Object instance | Class name |
Memory | Separate copy per instance | Single shared copy |
Use case | Object-specific data | Class-wide utilities |
๐ง Basic Syntax and Usage
๐ฆ Declaring Instance Members
Instance members are the default when you declare properties and methods:
class ShoppingCart {
// Instance properties ๐
items: string[] = [];
customerId: string;
constructor(customerId: string) {
this.customerId = customerId;
}
// Instance methods ๐๏ธ
addItem(item: string): void {
this.items.push(item);
console.log(`Added ${item} to ${this.customerId}'s cart! ๐`);
}
getItemCount(): number {
return this.items.length;
}
}
// Each cart is independent ๐
const aliceCart = new ShoppingCart("Alice");
const bobCart = new ShoppingCart("Bob");
aliceCart.addItem("๐ Apple");
aliceCart.addItem("๐ฅ Bread");
bobCart.addItem("๐ง Cheese");
console.log(aliceCart.getItemCount()); // 2
console.log(bobCart.getItemCount()); // 1
๐๏ธ Declaring Static Members
Static members use the static
keyword:
class MathHelper {
// Static constant ๐
static readonly PI = 3.14159;
// Static property ๐ข
static precision: number = 2;
// Static methods ๐งฎ
static round(value: number): number {
const factor = Math.pow(10, MathHelper.precision);
return Math.round(value * factor) / factor;
}
static degreesToRadians(degrees: number): number {
return degrees * (MathHelper.PI / 180);
}
// โ Cannot access static members with 'this' in static context
static incorrectMethod(): void {
// console.log(this.PI); // Error! ๐ซ
console.log(MathHelper.PI); // Correct! โ
}
}
// Using static members - no instantiation needed! ๐
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.round(3.14159)); // 3.14
console.log(MathHelper.degreesToRadians(90)); // 1.5708
// Changing static property affects all usage ๐
MathHelper.precision = 4;
console.log(MathHelper.round(3.14159)); // 3.1416
๐ก Practical Examples
๐ช Example 1: Product Inventory System
Letโs build a product inventory system that tracks individual products and overall statistics:
class Product {
// Instance properties ๐ฆ
id: string;
name: string;
price: number;
stock: number;
// Static properties for inventory management ๐
static totalProducts: number = 0;
static totalInventoryValue: number = 0;
private static productRegistry: Map<string, Product> = new Map();
constructor(id: string, name: string, price: number, stock: number) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
// Update static tracking ๐
Product.totalProducts++;
Product.totalInventoryValue += price * stock;
Product.productRegistry.set(id, this);
}
// Instance method - affects individual product ๐ฏ
updateStock(newStock: number): void {
const stockDifference = newStock - this.stock;
this.stock = newStock;
// Update total inventory value ๐ฐ
Product.totalInventoryValue += this.price * stockDifference;
console.log(`๐ฆ ${this.name} stock updated to ${this.stock}`);
}
// Instance method using static data ๐
getMarketShare(): number {
const myValue = this.price * this.stock;
return (myValue / Product.totalInventoryValue) * 100;
}
// Static method - class-level operation ๐๏ธ
static findProduct(id: string): Product | undefined {
return Product.productRegistry.get(id);
}
// Static method for reporting ๐
static getInventoryReport(): string {
return `
๐ Inventory Report:
- Total Products: ${Product.totalProducts}
- Total Value: $${Product.totalInventoryValue.toFixed(2)}
- Average Value per Product: $${(Product.totalInventoryValue / Product.totalProducts).toFixed(2)}
`;
}
}
// Creating products ๐๏ธ
const laptop = new Product("P001", "Gaming Laptop", 1200, 15);
const mouse = new Product("P002", "Wireless Mouse", 50, 100);
const keyboard = new Product("P003", "Mechanical Keyboard", 150, 50);
// Instance operations ๐ฎ
laptop.updateStock(20); // ๐ฆ Gaming Laptop stock updated to 20
console.log(`Laptop market share: ${laptop.getMarketShare().toFixed(2)}%`);
// Static operations ๐๏ธ
console.log(Product.getInventoryReport());
const foundProduct = Product.findProduct("P002");
console.log(`Found: ${foundProduct?.name}`); // Found: Wireless Mouse
๐ฎ Example 2: Game Session Manager
Hereโs a real-world example managing game sessions:
class GameSession {
// Instance properties ๐ฎ
sessionId: string;
playerId: string;
startTime: Date;
score: number = 0;
isActive: boolean = true;
// Static properties for session management ๐
private static activeSessions: Map<string, GameSession> = new Map();
private static sessionCounter: number = 0;
static readonly MAX_CONCURRENT_SESSIONS = 1000;
constructor(playerId: string) {
this.sessionId = `SESSION_${++GameSession.sessionCounter}`;
this.playerId = playerId;
this.startTime = new Date();
// Check session limit ๐ฆ
if (GameSession.activeSessions.size >= GameSession.MAX_CONCURRENT_SESSIONS) {
throw new Error("๐ซ Maximum concurrent sessions reached!");
}
GameSession.activeSessions.set(this.sessionId, this);
console.log(`๐ฏ New session started: ${this.sessionId} for player ${playerId}`);
}
// Instance method ๐ฏ
updateScore(points: number): void {
this.score += points;
console.log(`โญ Player ${this.playerId} scored ${points} points! Total: ${this.score}`);
}
// Instance method that interacts with static data ๐
endSession(): void {
this.isActive = false;
GameSession.activeSessions.delete(this.sessionId);
const duration = Date.now() - this.startTime.getTime();
console.log(`๐ Session ${this.sessionId} ended. Duration: ${duration / 1000}s, Score: ${this.score}`);
}
// Static utility methods ๐ ๏ธ
static getActiveSessionCount(): number {
return GameSession.activeSessions.size;
}
static getPlayerSession(playerId: string): GameSession | undefined {
for (const session of GameSession.activeSessions.values()) {
if (session.playerId === playerId && session.isActive) {
return session;
}
}
return undefined;
}
static endAllSessions(): void {
console.log("๐ Ending all active sessions...");
for (const session of GameSession.activeSessions.values()) {
session.endSession();
}
}
}
// Game flow example ๐ฎ
const aliceSession = new GameSession("Alice");
const bobSession = new GameSession("Bob");
aliceSession.updateScore(100);
aliceSession.updateScore(50);
bobSession.updateScore(200);
console.log(`Active sessions: ${GameSession.getActiveSessionCount()}`); // 2
// Find specific player's session ๐
const foundSession = GameSession.getPlayerSession("Alice");
foundSession?.updateScore(75);
// End individual session ๐
aliceSession.endSession();
console.log(`Active sessions: ${GameSession.getActiveSessionCount()}`); // 1
๐ Advanced Concepts
๐งฌ Static Members in Inheritance
Static members have special behavior in inheritance hierarchies:
class Animal {
// Static property ๐ฆด
static species: string = "Unknown";
// Static method ๐
static makeSound(): string {
return `The ${this.species} makes a sound!`;
}
// Instance property ๐ท๏ธ
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
// Override static property ๐
static species: string = "Canine";
// Add new static method ๐พ
static getBreedInfo(): string {
return `${this.species} - Man's best friend! ๐ฆด`;
}
}
class Cat extends Animal {
// Override static property ๐
static species: string = "Feline";
// Override static method with enhanced behavior ๐ญ
static makeSound(): string {
return `${super.makeSound()} Meow! ๐บ`;
}
}
// Static members are separate for each class ๐
console.log(Animal.species); // Unknown
console.log(Dog.species); // Canine
console.log(Cat.species); // Feline
console.log(Dog.makeSound()); // The Canine makes a sound!
console.log(Cat.makeSound()); // The Unknown makes a sound! Meow! ๐บ
console.log(Dog.getBreedInfo()); // Canine - Man's best friend! ๐ฆด
๐๏ธ Factory Pattern with Static Methods
Static methods are perfect for implementing factory patterns:
class User {
private constructor(
public id: string,
public username: string,
public email: string,
public role: string
) {}
// Static factory methods ๐ญ
static createAdmin(username: string, email: string): User {
const id = `ADMIN_${Date.now()}`;
console.log(`๐ Creating admin user: ${username}`);
return new User(id, username, email, "admin");
}
static createCustomer(username: string, email: string): User {
const id = `CUST_${Date.now()}`;
console.log(`๐๏ธ Creating customer: ${username}`);
return new User(id, username, email, "customer");
}
static createFromJSON(json: string): User {
const data = JSON.parse(json);
console.log(`๐ Creating user from JSON data`);
return new User(data.id, data.username, data.email, data.role);
}
// Static validation ๐ก๏ธ
static isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Instance method ๐ฏ
getProfile(): string {
return `๐ค ${this.username} (${this.role}) - ${this.email}`;
}
}
// Using factory methods ๐ญ
const admin = User.createAdmin("AdminAlice", "[email protected]");
const customer = User.createCustomer("BobCustomer", "[email protected]");
console.log(admin.getProfile()); // ๐ค AdminAlice (admin) - [email protected]
console.log(customer.getProfile()); // ๐ค BobCustomer (customer) - [email protected]
// Validation before creation ๐ก๏ธ
const email = "[email protected]";
if (User.isValidEmail(email)) {
const user = User.createCustomer("TestUser", email);
console.log("โ
User created successfully!");
}
๐ Private Static Members
Static members can also be private:
class ConfigManager {
// Private static configuration ๐
private static config: Map<string, any> = new Map();
private static isLocked: boolean = false;
// Public static methods to interact with private static data ๐
static set(key: string, value: any): void {
if (ConfigManager.isLocked) {
throw new Error("๐ Configuration is locked!");
}
ConfigManager.config.set(key, value);
console.log(`โ๏ธ Config set: ${key} = ${value}`);
}
static get(key: string): any {
return ConfigManager.config.get(key);
}
static lock(): void {
ConfigManager.isLocked = true;
console.log("๐ Configuration locked!");
}
static getSnapshot(): Record<string, any> {
const snapshot: Record<string, any> = {};
ConfigManager.config.forEach((value, key) => {
snapshot[key] = value;
});
return snapshot;
}
}
// Setting configuration โ๏ธ
ConfigManager.set("apiUrl", "https://api.example.com");
ConfigManager.set("timeout", 5000);
ConfigManager.set("retryAttempts", 3);
// Lock configuration ๐
ConfigManager.lock();
// Try to modify after lock
try {
ConfigManager.set("apiUrl", "https://new-api.com"); // Throws error! ๐ซ
} catch (error) {
console.log(error.message); // ๐ Configuration is locked!
}
// Reading is still allowed ๐
console.log(ConfigManager.get("apiUrl")); // https://api.example.com
console.log(ConfigManager.getSnapshot()); // Full config object
โ ๏ธ Common Pitfalls and Solutions
๐ซ Pitfall 1: Accessing Static Members via Instance
class Calculator {
static PI = 3.14159;
calculateCircleArea(radius: number): number {
// โ Wrong: Trying to access static member via instance
// return this.PI * radius * radius;
// โ
Correct: Access via class name
return Calculator.PI * radius * radius;
}
}
const calc = new Calculator();
// โ Wrong: Static members aren't available on instances
// console.log(calc.PI); // Property 'PI' does not exist
// โ
Correct: Access via class
console.log(Calculator.PI); // 3.14159
๐ซ Pitfall 2: Using โthisโ in Static Context
class Logger {
instanceId: string = `LOG_${Date.now()}`;
static globalLevel: string = "INFO";
static setLevel(level: string): void {
// โ Wrong: 'this' in static method refers to class, not instance
// console.log(this.instanceId); // Error!
// โ
Correct: Use class name for static members
Logger.globalLevel = level;
console.log(`๐ Log level set to: ${Logger.globalLevel}`);
}
logMessage(message: string): void {
// โ
Correct: Instance method can access both instance and static
console.log(`[${this.instanceId}][${Logger.globalLevel}] ${message}`);
}
}
๐ซ Pitfall 3: Shared Mutable Static State
// โ Problematic: Shared mutable array
class BadTaskManager {
static tasks: string[] = []; // Shared mutable state! ๐จ
addTask(task: string): void {
BadTaskManager.tasks.push(task);
}
}
// โ
Better: Immutable operations or proper encapsulation
class GoodTaskManager {
private static tasks: ReadonlyArray<string> = [];
private static taskMap: Map<string, string[]> = new Map();
constructor(private userId: string) {}
addTask(task: string): void {
const userTasks = GoodTaskManager.taskMap.get(this.userId) || [];
GoodTaskManager.taskMap.set(this.userId, [...userTasks, task]);
console.log(`โ
Task added for user ${this.userId}`);
}
static getAllTasks(): ReadonlyArray<string> {
const allTasks: string[] = [];
GoodTaskManager.taskMap.forEach(tasks => allTasks.push(...tasks));
return allTasks;
}
}
๐ ๏ธ Best Practices
1๏ธโฃ Use Static for Utility Functions
class StringUtils {
// โ
Good: Utility functions that don't need instance state
static capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
static truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.substring(0, maxLength - 3) + "...";
}
static countWords(str: string): number {
return str.trim().split(/\s+/).filter(word => word.length > 0).length;
}
}
// Clean usage without instantiation ๐
console.log(StringUtils.capitalize("hello")); // Hello
console.log(StringUtils.truncate("This is a long string", 10)); // This is...
console.log(StringUtils.countWords(" Hello world ")); // 2
2๏ธโฃ Use Instance for Object-Specific State
class BankAccount {
// โ
Good: Instance properties for account-specific data
private balance: number = 0;
private transactions: Array<{type: string, amount: number, date: Date}> = [];
constructor(
public accountNumber: string,
public accountHolder: string
) {}
deposit(amount: number): void {
this.balance += amount;
this.transactions.push({type: "deposit", amount, date: new Date()});
console.log(`๐ฐ Deposited $${amount}. New balance: $${this.balance}`);
}
getBalance(): number {
return this.balance;
}
getTransactionHistory(): string {
return this.transactions
.map(t => `${t.type}: $${t.amount} on ${t.date.toLocaleDateString()}`)
.join("\n");
}
}
3๏ธโฃ Combine Both for Complete Solutions
class TemperatureConverter {
// Static constants ๐ก๏ธ
static readonly CELSIUS_TO_FAHRENHEIT_RATIO = 9/5;
static readonly FAHRENHEIT_OFFSET = 32;
static readonly ABSOLUTE_ZERO_CELSIUS = -273.15;
// Instance property for preferred unit ๐
preferredUnit: "C" | "F" | "K";
constructor(preferredUnit: "C" | "F" | "K" = "C") {
this.preferredUnit = preferredUnit;
}
// Static utility methods ๐ ๏ธ
static celsiusToFahrenheit(celsius: number): number {
return celsius * this.CELSIUS_TO_FAHRENHEIT_RATIO + this.FAHRENHEIT_OFFSET;
}
static fahrenheitToCelsius(fahrenheit: number): number {
return (fahrenheit - this.FAHRENHEIT_OFFSET) / this.CELSIUS_TO_FAHRENHEIT_RATIO;
}
static celsiusToKelvin(celsius: number): number {
return celsius - this.ABSOLUTE_ZERO_CELSIUS;
}
// Instance method using static utilities ๐ฏ
convert(value: number, from: "C" | "F" | "K"): number {
let celsius: number;
// Convert to Celsius first
switch (from) {
case "C": celsius = value; break;
case "F": celsius = TemperatureConverter.fahrenheitToCelsius(value); break;
case "K": celsius = value + TemperatureConverter.ABSOLUTE_ZERO_CELSIUS; break;
}
// Convert to preferred unit
switch (this.preferredUnit) {
case "C": return celsius;
case "F": return TemperatureConverter.celsiusToFahrenheit(celsius);
case "K": return TemperatureConverter.celsiusToKelvin(celsius);
}
}
}
// Using both static and instance features ๐
const converter = new TemperatureConverter("F");
console.log(converter.convert(100, "C")); // 212 (100ยฐC to ยฐF)
console.log(TemperatureConverter.celsiusToKelvin(25)); // 298.15
๐งช Hands-On Exercise
Time to practice! ๐ฏ Create a library management system that tracks books and library statistics:
// Your challenge: Complete this implementation! ๐ช
class Book {
// Instance properties
isbn: string;
title: string;
author: string;
isAvailable: boolean = true;
borrowedBy?: string;
// Static tracking
static totalBooks: number = 0;
static availableBooks: number = 0;
private static bookRegistry: Map<string, Book> = new Map();
constructor(isbn: string, title: string, author: string) {
this.isbn = isbn;
this.title = title;
this.author = author;
// TODO: Update static counters
// TODO: Add to registry
}
// TODO: Implement borrowBook(memberName: string): boolean
// Should update availability and static counters
// TODO: Implement returnBook(): boolean
// Should update availability and static counters
// TODO: Implement static method findByISBN(isbn: string): Book | undefined
// TODO: Implement static method getLibraryStats(): string
// Should return formatted statistics
// TODO: Implement static method getMostPopularBooks(limit: number): Book[]
// Track borrow count and return most borrowed books
}
// Test your implementation! ๐งช
const book1 = new Book("978-1234567890", "TypeScript Mastery", "Jane Doe");
const book2 = new Book("978-0987654321", "Advanced Patterns", "John Smith");
console.log(Book.getLibraryStats());
book1.borrowBook("Alice");
console.log(Book.getLibraryStats());
๐ก Solution (click to reveal)
class Book {
isbn: string;
title: string;
author: string;
isAvailable: boolean = true;
borrowedBy?: string;
borrowCount: number = 0;
static totalBooks: number = 0;
static availableBooks: number = 0;
private static bookRegistry: Map<string, Book> = new Map();
constructor(isbn: string, title: string, author: string) {
this.isbn = isbn;
this.title = title;
this.author = author;
Book.totalBooks++;
Book.availableBooks++;
Book.bookRegistry.set(isbn, this);
console.log(`๐ New book added: "${title}" by ${author}`);
}
borrowBook(memberName: string): boolean {
if (!this.isAvailable) {
console.log(`โ "${this.title}" is already borrowed by ${this.borrowedBy}`);
return false;
}
this.isAvailable = false;
this.borrowedBy = memberName;
this.borrowCount++;
Book.availableBooks--;
console.log(`โ
"${this.title}" borrowed by ${memberName}`);
return true;
}
returnBook(): boolean {
if (this.isAvailable) {
console.log(`โ "${this.title}" is not currently borrowed`);
return false;
}
const borrower = this.borrowedBy;
this.isAvailable = true;
this.borrowedBy = undefined;
Book.availableBooks++;
console.log(`โ
"${this.title}" returned by ${borrower}`);
return true;
}
static findByISBN(isbn: string): Book | undefined {
return Book.bookRegistry.get(isbn);
}
static getLibraryStats(): string {
const borrowedBooks = Book.totalBooks - Book.availableBooks;
const availabilityRate = (Book.availableBooks / Book.totalBooks * 100).toFixed(1);
return `
๐ Library Statistics:
- Total Books: ${Book.totalBooks}
- Available: ${Book.availableBooks}
- Borrowed: ${borrowedBooks}
- Availability Rate: ${availabilityRate}%
`;
}
static getMostPopularBooks(limit: number): Book[] {
const books = Array.from(Book.bookRegistry.values());
return books
.filter(book => book.borrowCount > 0)
.sort((a, b) => b.borrowCount - a.borrowCount)
.slice(0, limit);
}
}
๐ Key Takeaways
Youโve mastered the distinction between instance and static members! Hereโs what youโve learned:
-
Instance Members ๐ฏ:
- Belong to individual objects
- Store object-specific state
- Access via object instances
- Perfect for data that varies between objects
-
Static Members ๐๏ธ:
- Belong to the class itself
- Shared across all instances
- Access via class name
- Ideal for utilities and shared state
-
Best Practices ๐:
- Use static for utility functions and constants
- Use instance for object-specific data
- Be careful with shared mutable static state
- Combine both for complete solutions
-
Common Patterns ๐จ:
- Factory methods
- Singleton implementation
- Registry/tracking systems
- Configuration management
๐ค Next Steps
Congratulations on completing this tutorial! ๐ You now understand one of the fundamental concepts in object-oriented programming. Hereโs what to explore next:
- ๐ Getters and Setters: Learn how to control property access
- ๐ Method Chaining: Create fluent interfaces
- ๐ญ Abstract Classes: Design blueprint classes
- ๐งฌ Inheritance: Extend classes effectively
Keep practicing, and remember: the choice between instance and static is about asking โDoes this belong to each object or to the concept itself?โ ๐ค
Happy coding! ๐โจ