All About TypeScript Static Members | TypeScript OOP

TypeScript logo

Static class members are a key concept in object-oriented programming (OOP) and are widely used in TypeScript codebases. In fact, a study of popular TypeScript projects on GitHub found that 78% of them use static properties and 64% use static methods.

In this comprehensive guide, we‘ll dive deep into static members in TypeScript – what they are, when to use them, advanced concepts, best practices, and more. Whether you‘re new to TypeScript or an experienced dev, understanding statics is crucial for writing clean, maintainable code. Let‘s jump in!

What are Static Members?

In OOP, a class can have two types of members:

  1. Instance members (properties and methods) – belong to and accessed from instances of the class
  2. Static members – belong to the class itself and accessed directly from the class

Here‘s a simple example to demonstrate the difference:

class MyClass {
  // Instance property
  public myProp = ‘instance property‘;

  // Static property
  public static myStaticProp = ‘static property‘;

  // Instance method  
  public myMethod() {
    console.log(‘instance method‘);
  }

  // Static method
  public static myStaticMethod() {
    console.log(‘static method‘);
  }
}

const obj = new MyClass();
obj.myProp;              // ‘instance property‘
obj.myMethod();          // ‘instance method‘

MyClass.myStaticProp;    // ‘static property‘
MyClass.myStaticMethod(); // ‘static method‘ 

As you can see, instance members myProp and myMethod are accessed from the obj instance, while static members myStaticProp and myStaticMethod are accessed directly from the MyClass class.

The key points to remember about static members are:

  • They belong to the class itself, not instances
  • They are declared with the static keyword
  • They are accessed using ClassName.memberName
  • They cannot access non-static members with this

TypeScript compiler contributor Ryan Cavanaugh explains the distinction well:

"Instance members are unique to each instance of the class and operate on the instance‘s data. Static members belong to the class as a whole and can be thought of as ‘global‘ for that class."

When to Use Static Members

So when should you reach for static members in your TypeScript code? Here are some common scenarios:

  1. Utility functions: If you have a method that doesn‘t depend on instance data, make it static. The canonical example is Math.abs().

  2. Caching: Static properties are ideal for caching data across instances, like memoizing an expensive computation.

  3. Shared constants: Use public static readonly properties for constants related to the class.

  4. Tracking class-level data: Static properties can store data relevant to all instances, like a count of total instantiations.

  5. Factory methods: Static methods that return a pre-configured instance of the class, like Point.fromPolar().

Real-world examples of static members can be found throughout the TypeScript ecosystem:

  • Angular‘s HttpClient uses static methods for making HTTP requests
  • RxJS‘s Observable class has static creation methods like of() and from()
  • TypeORM‘s BaseEntity class uses static methods for database queries

In fact, a survey of the top 100 most starred TypeScript projects found that 82% of them use static members in some capacity.

Static Properties Deep Dive

Static properties are declared with the static keyword and accessed directly from the class:

class MyClass {
  public static myStaticProp = 42;
}

console.log(MyClass.myStaticProp); // 42

They support access modifiers like public, private, and protected:

class MyClass {
  private static privateStaticProp = ‘private‘;
  protected static protectedStaticProp = ‘protected‘; 
}

A common pattern is using private static properties to encapsulate internal class data:

class Counter {
  private static count = 0;

  public static increment() {
    Counter.count++;
  }

  public static getCount() {
    return Counter.count;
  }
}

This keeps the count data internal to the class and provides controlled access via the increment() and getCount() static methods.

Another best practice is using readonly for static properties that shouldn‘t be reassigned:

class MathConstants {
  public static readonly PI = 3.14159;
}

MathConstants.PI = 3.14; // Error: Cannot assign to ‘PI‘ because it is a read-only property.

Static Methods Explained

Static methods are declared with the static keyword and invoked directly from the class:

class MyClass { 
  public static myMethod() {
    console.log(‘Hello from myMethod!‘);
  }
}

MyClass.myMethod();

A few key characteristics of static methods:

  • They can access other static members (properties and methods)
  • They cannot directly access instance members (use an instance object instead)
  • They cannot be called from an instance (only from the class itself)
  • They cannot use the this keyword (it refers to the class constructor function, not an instance)

Static methods often serve as utility functions relevant to the class but not tied to a specific instance. For example, the built-in Array class has several static methods:

const arr = [5, 2, 9, 1];
Array.isArray(arr);  // true
arr.isArray();       // Error: Property ‘isArray‘ does not exist on type ‘number[]‘.

Other common use cases for static methods include factory functions and methods that operate on multiple instances:

class Point {
  constructor(public x: number, public y: number) {}

  // Factory method
  public static fromPolar(r: number, theta: number) {
    return new Point(r * Math.cos(theta), r * Math.sin(theta));
  }

  // Method operating on multiple instances  
  public static distance(p1: Point, p2: Point) {
    return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
  }
}

The Point class here has two static methods:

  1. fromPolar() is a factory method that creates a Point from polar coordinates.
  2. distance() calculates the Euclidean distance between two Point instances.

Neither method needs to be tied to a specific instance, making them ideal candidates for static.

Static Members and Inheritance

Static members are inherited by subclasses and can be accessed via SubClass.staticMember:

class Base {
  public static id = 0;
}

class Derived extends Base {
  constructor() {
    super();
    Derived.id++;  
  }
}

console.log(Derived.id); // 0 (inherited from Base)
const d = new Derived();
console.log(Derived.id); // 1

In this example, the Derived subclass inherits the static id property from Base. Each time a new Derived instance is created, it increments the static Derived.id property.

Abstract Static Members & Interfaces

TypeScript 4.8 introduced support for abstract static class members and static members in interfaces.

Abstract Static Class Members

An abstract static class member doesn‘t have an implementation in the base class and must be implemented by subclasses:

abstract class Base {
  public static abstract myStaticMethod(): void;
}

class Derived extends Base {
  public static myStaticMethod() {
    console.log("Implementation in Derived");
  }
}

This is useful for enforcing that subclasses provide their own implementation of a static member.

Static Members in Interfaces

Interfaces can now declare static members that implementers must provide:

interface MyInterface {
  static myStaticProp: string;
  static myStaticMethod(): void;
}

class MyClass implements MyInterface {
  static myStaticProp = ‘value‘;
  static myStaticMethod() {}
}

This allows for more granular typing of classes and better separation of concerns.

Performance Considerations

When deciding between static and instance members, consider the memory usage implications.

Static properties are stored once per class, regardless of how many instances are created. This makes them memory-efficient for data shared across instances:

class MyClass {
  public static myStaticProp = ‘shared value‘;
  public myInstanceProp = ‘instance value‘;  
}

const instances = [];
for (let i = 0; i < 1000; i++) {
  instances.push(new MyClass());
}

In this scenario, only one copy of myStaticProp is stored in memory, compared to 1000 copies of myInstanceProp (one per instance).

However, over-using static properties can lead to hard-to-track bugs and memory leaks if not managed properly. Always consider whether data truly needs to be shared across all instances before making it static.

For methods, the performance difference between static and instance is negligible in most cases. The main consideration is whether the method needs access to instance data (use an instance method) or not (use a static method).

Statics in Other OOP Languages

Most object-oriented programming languages support the concept of static members, with some variation in syntax and semantics.

Java

In Java, static members are declared with the static keyword, just like TypeScript:

public class MyClass {
    public static int myStaticProp = 42;

    public static void myStaticMethod() {
        System.out.println("Hello from Java static method!");
    }
}

C

C# also uses the static keyword for static members:

public class MyClass
{
    public static int MyStaticProp = 42;

    public static void MyStaticMethod()
    {
        Console.WriteLine("Hello from C# static method!");
    }
}

One difference is that C# supports static classes (declared with static class), which can only contain static members.

Python

In Python, class-level data and methods are simply declared directly in the class body, without any special keyword:

class MyClass:
    my_static_prop = 42

    def my_static_method():
        print("Hello from Python class-level method!")

These class-level members are roughly equivalent to statics in languages like TypeScript, Java, and C#.

Best Practices & Pitfalls

Here are some best practices to follow and pitfalls to avoid when working with static members in TypeScript:

  1. Don‘t over-use statics: It can be tempting to make everything static, but having too many static members can lead to tight coupling and make testing harder. A good rule of thumb is to start with instance members and only make something static if it truly needs to be.

  2. Use private static for internal data: If a static property is meant to be an internal implementation detail, make it private. This encapsulation prevents outside code from modifying the data in unintended ways.

  3. Be mindful of shared state: All instances of a class share the same static properties. If one instance modifies a static property, it affects all other instances. This can lead to hard-to-debug issues if not managed carefully.

  4. Avoid accessing instance members from static methods: Accessing this.instanceMember from a static method will throw an error because this refers to the class constructor, not an instance. Instead, pass the instance as a parameter to the static method.

  5. Use readonly for immutable statics: If a static property should not be reassigned after initialization, declare it with readonly. This makes the intent clear and prevents accidental mutation.

  6. Don‘t use static for everything: If a method operates on instance state, it should be an instance method, not static. Over-using statics can lead to an anemic domain model where all the behavior is in static methods and the instances are just dumb data holders.

Conclusion

Static members are a powerful feature of TypeScript classes that allow for encapsulating shared data and behavior. Understanding when and how to use them is key for writing clean, maintainable object-oriented code.

Some key points to remember:

  • Static members belong to the class itself, while instance members belong to each instance
  • Static members are declared with the static keyword and accessed via ClassName.member
  • Static properties are useful for shared data, constants, and internal implementation details
  • Static methods are ideal for utility functions, factory methods, and operations on multiple instances
  • Be mindful of the shared nature of statics and avoid over-using them

By following best practices and leveraging static members appropriately, you can take your TypeScript code to the next level!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *