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 0

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

Because the properties can only be accessed through the class itself, we can't do:

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.

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, as yourself:

Will this property ever need to be used by another class, without having an instance of this class?

In other words, should I need to call it on an object created by this class? 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}`);
  }
}

Advanced TypeScript & Node.js blog

If you enjoyed this article, you should check out my blog. I write about Advanced TypeScript & Node.js best practices for large-scale applications and teach developers how to write flexible, maintainable software.