All About TypeScript Static Members | TypeScript OOP
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:
- Instance members (properties and methods) – belong to and accessed from instances of the class
- 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:
-
Utility functions: If you have a method that doesn‘t depend on instance data, make it static. The canonical example is
Math.abs()
. -
Caching: Static properties are ideal for caching data across instances, like memoizing an expensive computation.
-
Shared constants: Use public static readonly properties for constants related to the class.
-
Tracking class-level data: Static properties can store data relevant to all instances, like a count of total instantiations.
-
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 likeof()
andfrom()
- 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:
fromPolar()
is a factory method that creates aPoint
from polar coordinates.distance()
calculates the Euclidean distance between twoPoint
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:
-
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.
-
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.
-
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.
-
Avoid accessing instance members from static methods: Accessing
this.instanceMember
from a static method will throw an error becausethis
refers to the class constructor, not an instance. Instead, pass the instance as a parameter to the static method. -
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. -
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 viaClassName.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!