All Articles

Four Essential TypeScript Patterns You Can't Work Without

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

There’s a difference between using TypeScript and knowing TypeScript.

The docs give you a good grasp of the pieces like generic functions, conditional types, and type helpers.

But out in the wild, developers are combining these pieces together into patterns.

Four of the most important patterns to know and use are:

  • Branded types
  • Globals
  • Assertion Functions & Type Predicates
  • Classes (yes, really!)

Let’s dive in!

Here’s some example code that uses a branded type:

type Password = Brand<string, ‘Password’>;
const takesInPassword = (password: Password) => {}
// Will error! takesInPassword(‘4123123’)
// Won’t error! takesInPassword(‘4123123’ as Password)

Branded types let you assign different ‘labels’ to values. In the code above, the Password type can only be satisfied by something that has been branded as Password.

This creates validation boundaries in your app’s code - you can validate that something is a valid password, then safely pass it around your application.

The second is globals.

Understanding how the ‘global type scope’ works in TypeScript is critical. Love them or hate them, globals are a part of the way we use JavaScript. And if we use them in JavaScript, we need to be able to describe them in TypeScript.

For instance: declare global lets you type functions and variables directly in the global scope:

declare global { function myFunc(): boolean; var myVar: number; }
console.log(myVar); console.log(myFunc());

You can use similar techniques to add types to process.env, type Window, and even create your own global types.

The third pattern is assertion functions and type predicates.

Type predicates let you specify that a function returns a boolean which describes one of the arguments passed to it:

const values = [“a”, “b”, undefined, “c”, undefined];
const filteredValues = values.filter((value): value is string => Boolean(value), );
// filteredValues is now string[]

And assertion functions work similarly, but let you throw an error inside a function to ‘assert’ that the value passed corresponds to a certain type.

function assertUserIsAdmin(
  user: NormalUser | AdminUser,
): asserts user is AdminUser {
  if (user.role !== "admin") {
    throw new Error("Not an admin user");
  }
}

Both of these patterns let you improve TypeScript’s ability to narrow your code.

The fourth pattern is ****classes****.

Classes?!

Yes.

The humble ES6 class, in TypeScript’s hands, becomes an amazing tool for enacting the builder pattern. This design is at the core of tRPC, one of TypeScript’s most popular libraries.

Combined with assertion functions, you can even use it to type the shape of the class from inside the class itself.

export class SDK {
  loggedInUser?: User;

  constructor(loggedInUser?: User) {
    this.loggedInUser = loggedInUser;
  }

  assertIsLoggedIn(): asserts this is this & { loggedInUser: User } {
    if (!this.loggedInUser) {
      throw new Error("Not logged in");
    }
  }
}

Classes are often misaligned and misunderstood.

“I thought we used hooks now??”

Look, when you’re writing TS in library-land, classes are a force of pragmatic utility that you can’t sleep on.

Having a shared set of core patterns is an absolute super power for any team, and that’s what the Advanced Patterns Workshop will do for you. This Advanced Patterns workshop is available now as part of the Total TypeScript Core Volume!

Matt's signature

Share this article with your friends