No special messages at the moment. Just have a good day and stay hydrated!
Close

All about TypeScript Static Members | TypeScript OOP

Jul 7th, 2019 / 6 min read / Share / Edit on GitHub
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 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}`);
  }
}


Sponsor

I hope this article was useful to you! Consider checking out my sponsors. I can continue to write quality articles for free because of them.

Discussion

Thoughts? Share the article if you think it'll be useful to someone + join the discussion about this post on Twitter!


Stay in touch!



About the author

Khalil Stemmler

Khalil Stemmler is a Developer / Designer and co-founder of Univjobs. He frequently publishes articles about Domain-Driven 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

Implementing DTOs, Mappers & the Repository Pattern using the Sequelize ORM [with Examples] - DDD w/ TypeScript
Jun 20th, 2019 / 14 min read
There are several patterns that we can utilize in order to handle data access concerns in Domain-Driven Design. In this article, w...
Understanding Domain Entities [with Examples] - DDD w/ TypeScript
May 28th, 2019 / 12 min read
Entities are the first natural place we should aim to place business logic in domain-driven applications. In this article, we talk...
REST-first design is Imperative, DDD is Declarative [Comparison] - DDD w/ TypeScript
May 15th, 2019 / 11 min read
A comparison between designing Node.js applications using REST-first design and Domain-Driven Design.
An Introduction to Domain-Driven Design - DDD w/ TypeScript
Apr 9th, 2019 / 10 min read
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 2000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates