All about TypeScript Static Members | TypeScript OOP

In this blog post, I explain the static keyword and when you might want to make attributes and methods a member of the class, rather than an instance of the class.

In Object-Oriented Programming, we write a lot of classes.

Classes contain properties (methods and attributes) which hold variables and operations.

Every time we define the properties of a class, they are said to belong to either:

  • an instance of the class (an object created via constructor) OR
  • the class itself (we call this a class member)

What do we mean by that?

How can properties belong to only the instance vs. only the class?

When we choose to use or omit the static keyword, it changes who the properties belong to.

Let's look at regular usage without the static keyword.

Regular usage (properties belong to the instance)

Normally, when we define properties on a class, the only time they can be accessed is after we've created an instance of that class or if we use this to refer to the properties that will eventually reside on an instance of the object.

Take this early example from White Label.

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  } 

  public printSummary (): void {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

const vinyl = new Vinyl('Goo', 'Sonic Youth', ['rock']);
console.log(vinyl.title)    // 'Goo'
console.log(vinyl.artist)   // 'Sonic Youth'
console.log(vinyl.genres)   // ['rock']
vinyl.printSummary();	      // 'Goo is an album by Sonic Youth'

Each of the methods (printSummary(): void) and attributes (title, artist, genres) on the Vinyl class are said to belong to an instance of the class.

In the example, we were only able to access the properties title, artist and genres directly from the object after it was created.

console.log(vinyl.title)    // This is valid!

Also note that when we use printSummary(): void, we can access title and artist using the this keyword:

class Vinyl {
  ...
  public printSummary (): void {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

That works because at this point, the resulting object / instance of Vinyl owns those properties.

If we check out TypeScript Playground, we can look at the compiled JavaScript for this code sample:

"use strict";
class Vinyl {
  constructor(title, artist, genres) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }
  printSummary() {
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

const vinyl = new Vinyl('Goo', 'Sonic Youth', ['rock']);
console.log(vinyl.title); // 'Goo'
console.log(vinyl.artist); // 'Sonic Youth'
console.log(vinyl.genres); // ['rock']
vinyl.printSummary(); // 'Goo is an album by Sonic Youth'

The resulting JavaScript looks nearly identical.

Let's talk a bit about what happens when the properties are owned by the class.

Static properties (properties belong to the class)

When we use the static keyword on properties we define on a class, they belong to the class itself.

That means that we cannot access those properties from an instance of the class.

We can only access the properties directly by referencing the class itself.

To demonstrate, let's add a counter NUM_VINYL_CREATED that increments the number of times that a Vinyl was created.

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static NUM_VINYL_CREATED: number = 0;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;

	Vinyl.NUM_VINYL_CREATED++;        // increment number of vinyl created
    console.log(Vinyl.NUM_VINYL_CREATED)  
  } 

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

let goo = new Vinyl ('Goo', 'Sonic Youth', ['rock']);
// prints out 1

let daydream = new Vinyl ('Daydream Nation', 'Sonic Youth', ['rock']);
// prints out 2

Because the properties can only be accessed through the class by referring to the class name itself, we can't access static properties through an instance like so:

let goo = new Vinyl ('Goo', 'Sonic Youth', ['rock']);
goo.MAX_GENRES_PER_VINYL    // Error
goo.NUM_VINYL_CREATED       // Error

You might have heard of a term called Class Members. An attribute or a method is a class member because they can ONLY be accessed through the class itself; therefore, they're members of the class.

class Vinyl {

  /**
   * Example "Class Member" (sometimes known as 
   * "Static Class Member") variable.
   * 
   * Can only be accessed through mentioning the class name
   * itself:
   * 
   * Example: const vinylCreatedToDate = Vinyl.NUM_VINYL_CREATED;
   */

  public static NUM_VINYL_CREATED: number = 0; 

  /**
   * Example "Instance Member" variable.
   * 
   * Can only be accessed through an instance/object created from 
   * this class.
   * 
   * Example: 
   *  
   *  const blueAlbum = new Vinyl("Blue Album");
   *  console.log(blueAlbum.title); // "Blue Album"
   */

  public title: string;

  constructor (title: string) {
    this.title = title;
  }
}

The different types of member variables that can appear on a class.

That's great and all, but when would you want to use static properties?

How to know when to use static properties

Before you add that attribute or method, ask yourself:

Will this property ever need to be used by another class, without first needing to create an object of it?

In other words, should I need to call it on an object created by this class or not? If yes, then continue normally.

If no, then you might want to make a static member.

Scenarios where it could make sense to use a static property

Scenarios where it seems like it might make sense but actually leads to an anemic domain model:

  • to perform validation logic on atttributes for that class (use Value Objects instead)

To demonstrate a worthwhile scenario, let's add a static MAX_GENRES_PER_VINYL attribute to "document a constraint" that a Vinyl may only have at max 2 different types of Genres.

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static MAX_GENRES_PER_VINYL: number = 2;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}

And then let's add an addGenre(genre: Genre): void method to enforce this business rule.

type Genre = 'rock' | 'pop' | 'electronic' | 'rap'

class Vinyl {
  public title: string;
  public artist: string;
  public genres: Genre[];
  public static MAX_GENRES_PER_VINYL: number = 2;

  constructor (title: string, artist: string, genres: Genre[]) {
    this.title = title;
    this.artist = artist;
    this.genres = genres;
  }

  public addGenre (genre: Genre): void {
    // Notice that in order to reference the value, we have go through the class
    // itself (Vinyl), not through an instance of the class (this).
    const maxLengthExceeded = this.genres.length >= Vinyl.MAX_GENRES_PER_VINYL;
    const alreadyAdded = this.genres.filter((g) => g === genre).length !== 0;

    if (!maxLengthExceeded && !alreadyAdded) {
      this.genres.push(genre);
    }
  }

  public printSummary (): void { 
    console.log(`${this.title} is an album by ${this.artist}`);
  }
}


Discussion

Liked this? Sing it loud and proud 👨‍🎤.


1 Comment

Submit
Pablo Yeverino
4 months ago

Hi! I was asked to avoid static members of a class and instead use an internal variable from my module. I cannot get why? Do you have any idea?


Thanks for all your shared knowledge

Khalil Stemmler
4 months ago

I guess it depends on the use case. A soul once told me software design can be reduced to three things: state objects, behaviours, and namespaces.


I prefer static members (attributes and methods) over stray variables and methods because they're more explicitly namespaced.


For example, take this `TextUtils` class.


export class TextUtils {
  public static replaceAll (str: string, match: string): string {
    ...
  }
}


Now, anytime there's a new text utility method, it has a place that it belongs. Compare that to the following.


export function replaceAll (str: string, match: string): string { ... }


There's no real namespace here. Maybe the file that holds it is called `TextUtils.ts` but that's not statically analyzed. I prefer the first solution because it can help to keep code DRY. The intent of the method, and any future methods, is expressed by the classname.


As for your actual question, why would we want to avoid using static members?


I'm not sure. There's nothing dangerous about it.









Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in TypeScript



You may also enjoy...

A few more related articles

How to Handle Updates on Aggregates - Domain-Driven Design w/ TypeScript
In this article, you'll learn approaches for handling aggregates on Aggregates in Domain-Driven Design.
Decoupling Logic with Domain Events [Guide] - Domain-Driven Design w/ TypeScript
In this article, we'll walk through the process of using Domain Events to clean up how we decouple complex domain logic across the...
Does DDD Belong on the Frontend? - Domain-Driven Design w/ TypeScript
Should we utilize Domain-Driven Design principles and patterns in front-end applications? How far does domain modeling reach from ...
An Introduction to Domain-Driven Design - DDD w/ TypeScript
Domain-Driven Design is the approach to software development which enables us to translate complex problem domains into rich, expr...

Want to be notified when new content comes out?

Join 5000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates