3 Useful TypeScript Patterns to Keep in Your Back Pocket

Using TypeScript with its most basic types alone adds plenty of safety to your code. Sometimes, however, you come across situations where you need a little bit more. Here are three useful TypeScript patterns that will kick your game up a notch.

Pattern 1: Mapped Types

A mapped type allows us to create a type that defines an object’s contents. Its definition looks like this:

type MappedType = {
  [Key in T]: V
};

Here, the “T” refers to the type, which is the set of all keys, and “V” is the type which is the set of all values. Here’s an example:


type Names = "Bob" | "Bill" | "Ben";
type JobTitles = "Welder" | "Carpenter" | "Plumber";

const JobAssignments: { [Key in Names]: JobTitles } = {
  Bob: "Welder",
  Bill: "Carpenter",
  Ben: "Plumber"
};

Note that TypeScript will throw an error if one of the fields is removed. This is because none of the fields has been set as optional in our type definition. We can change this by adding a question mark in our type definition like this:

{ [Key in Names]?: JobTitles }

Typescript also includes several built-in types that are defined using mapped types. Some of the more common ones include Pick, Record, and Readonly. The previous example where we set all of the fields to be optional also exists as a built-in type called Partial.

Pattern 2: Function Overloading

Say we want to write a function that, upon being given a string or number, returns that value doubled. Specifically, if we give it 2, we want 4. If we give it “hello!”, we want “hello!hello!”

This is pretty simple. The function will look like this:

function inputDoubler(input: string | number) {
  if (typeof input === "string"){
    return `${input}${input}`;
  } else {
    return input * 2;
  }
};

Later down the line, we want to add two doubled numbers together, so we do something like this:

inputDoubler(5) + inputDoubler(10);

But this won’t compile! TypeScript has inferred from inputDoubler that the possible return values are number and string, but it doesn’t know which of the two are being returned here. It is unable to infer the specific return value from the argument.

One easy way we can solve this problem is by using function overloading. Before we even define inputDoubler, we can define two signatures like this:

function inputDoubler(input: string): string;
function inputDoubler(input: number): number;

This tells TypeScript that whenever we call inputDoubler with a string, we will be returning a string. When we call it with a number, we will be returning a number. The code in its entirety now looks like this:

function inputDoubler(input: string): string;
function inputDoubler(input: number): number;

function inputDoubler(input: string | number) {
  if (typeof input === "string"){
    return `${input}${input}`;
  } else {
    return input * 2;
  }
};

inputDoubler(5) + inputDoubler(10);

This works! TypeScript now knows that, because we’ve passed a number to both of our inputDoubler calls, we will be getting a number back, and it can now perform the addition without a hitch.

This also helps give us a more descriptive error message if we used this function incorrectly. Say we tried doing this:

inputDoubler("five") + inputDoubler("ten");

If we hadn’t done our function overloading, we’d receive the same generic error message we talked about earlier, where TypeScript can’t tell if we’re working with strings or numbers. However, because we’ve explicitly set up our function signatures, we know right off the bat that we’re getting two strings and trying to erroneously add them together.

Pattern 3: Custom Type Guards

TypeScript is good about inferring the type you’re using after doing a check on it. For example, say we have the following two types:

type Pizza = {
  ingredients: {
    topping: string
  }
};

type Burrito = {
  ingredients: {
    filling: string
  }
};

We then might have a function like this:

function printIngredients (food: Pizza | Burrito) {
  if (food.ingredients.topping) {
    console.log(food.ingredients.topping);
  } else {
    console.log(food.ingredients.filling);
  };
};

This all works just fine. Typescript doesn’t throw errors in the logs because it can infer which type food is from the if statement. Now let’s say we want to clean up our code a bit and add an isPizza function:

function isPizza (food: Pizza | Burrito) {
  return food.ingredients.topping !== undefined;
};

function printIngredients (food: Pizza | Burrito) {
  if (isPizza(food)) {
    console.log(food.ingredients.topping);
  } else {
    console.log(food.ingredients.filling);
  };
};

Suddenly this doesn’t work anymore. TypeScript is no longer able to infer the type of food from the if statement. Why not?

The problem is we are checking the existence of the topping field in isPizza, which is a different scope from printIngredients. TypeScript can’t bring its type inference outside back to printIngredients. The solution here is to make a small tweak to our isPizza function:

function isPizza (food: Pizza | Burrito): food is Pizza {
  return food.ingredients.topping !== undefined;
};

isPizza is now a type guard. The return type of isPizza is still boolean, but with the added effect of bringing the type inference out of isPizza to wherever it is used.


As we’ve seen from these examples, TypeScript’s power goes much further than simple type definitions and checks, and we’ve only just scratched the surface. Whenever you run into a problem that TypeScript doesn’t seem capable of solving, do some digging into all of its capabilities. It very likely has some tricks that will give you a solution.

Conversation
  • Jeff Dazé says:

    One thing I noticed is that the overloaded function using ‘+’ should actually work since that’s used for concatenation of strings — so the complier won’t complain about that and should happily return “fivefivetenten” using your example. What the compiler will indeed complain about is if you used any other numeric operator such as subtraction ‘-‘ as expected.

    Excellent set of tricks for dealing with some interesting use cases for Typescript; thank you for the article!

  • Join the conversation

    Your email address will not be published. Required fields are marked *