Skip to content

Instantly share code, notes, and snippets.

@pesterhazy
Last active November 28, 2024 12:40
Show Gist options
  • Save pesterhazy/ce459c491cd9f8b5772289b23a56bde6 to your computer and use it in GitHub Desktop.
Save pesterhazy/ce459c491cd9f8b5772289b23a56bde6 to your computer and use it in GitHub Desktop.
Why Use Classes in TypeScript?

Why use classes in TypeScript?

There are many reasons why people like Object-Oriented Programming, but personally, my main reason for the use of classes in TypeScript is making dependencies explicit and replaceable. For example, suppose you have a module, Calendar, which depends on another module, GCalClient - i.e. Calendar uses the functionality from GCalClient but not vice versa. The most straightforward way to write that code is by simply using the functionality from GCalClient when needed:

// calendar.ts

import {bookAppointment} from "google-calendar-client"

function createMeeting() {
  bookAppointment();
}

// google-calendar-client.ts

function bookAppointment() {
}

In fact there's absolutely nothing wrong with writing it this way, and usually that's the first thing I'll do. However, eventually I'll run into a situation where I want to be able to use a Calendar with a GCalClient that's configured differently than normal. In particular, I want to test Calendar while providing a special GCalClient that doesn't actually make any requests to external HTTP servers but instead is hard-wired to return a pre-configured response.

At that point, I'll typically refactor the code to use classes:

// calendar.ts

import {GCalClient} from "google-calendar-client"

export class Calendar {
  gCalClient: GCalClient;
  
  constructor({gCalClient}) {
    this.gCalClient = gCalClient;
  }

  createMeeting() {
    this.gCalClient.bookAppointment();
  }
  
  static create() {
    return new Calendar({gCalClient: GCalClient.create()});
  }
}

// google-calendar-client.ts

export class GCalClient {
  bookAppointment() {
  }
  
  static create() {
    return new GCalClient();
  }
}

This gives me a few benefits:

  • I can see at a glance what dependencies Calendar has: it clearly depends on a GCalClient.
  • In production code, I don't need to care about any of this. As a consumer, I can just call Calendar.create().createMeeting() without worrying about GCalClients - the fact that Calendar depends on GCalClient is a mere implementation detail that's hidden. From a consumer standpoint, all that changes is that I need to call Calendar.create().createMeeting() rather than createMeeting().
  • But when writing a test, I can now replace the GCalClient dependency with a special GCalClient that doesn't make HTTP requests to the cloud. This is incredibly useful because it allows me to write a meaningful test that runs in milliseconds (rather than seconds) without relying on any out-of-process dependencies.

What's the alternative? I could provide the dependencies as regular function parameters:

function create(gCalClient: GCalClient, moreDeps..., phoneNumber: number, moreArgs...) {
}

But while that works, it has downsides compared to including dependencies as constructor arguments. Passing dependencies as function arguments makes the function signature hard to read, especially when there are many dependencies (because it mixes dependencies and runtime arguments). And it makes it trickier to create alternate versions of dependencies (because I don't have a class to clearly specify what the contract is). So while classes are not always my first choice in software design, I will write classes to manage dependencies, even if that means having to deal with more boilerplate (which by the way LLMs are very good at writing).

That's really all there is to it. Classes used in this way - as a container for functions bundled with dependencies - help with testing. What's more, they have a strong positive effect on code quality, because they make dependencies clear and explicit and guide you towards being mindful about what dependencies a class should or shouldn’t have. That's a big deal, because awkward dependencies and the related concept of high coupling are the number one factor why code is expensive to change – with real economic consequences for the business.

Further Reading

James Shore, Dependency Injection Demystified

@sebhs
Copy link

sebhs commented Nov 21, 2024

great write up!! thanks a lot

@LilaStone
Copy link

LilaStone commented Nov 21, 2024

Working through code challenges reminds me of the thrill of gambling—taking calculated risks to achieve the perfect outcome. Similarly, casinos that use Inclave offer a unique and secure experience for those who enjoy gaming with innovative features. Exploring new frameworks in coding is a lot like exploring new systems in gaming, you discover better ways to enjoy the process while staying efficient. For those curious, you can learn more at https://uk.notgamstop.com/casino-features/inclave-casinos/ site link. Both pursuits keep you engaged and push your skills to the next level!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment