5 useful Typescript tricks

Oct 6, 2019
Last update: Sep 22, 2020
~ 5 min

Typescript is a godsend. It is very easy to get started with and for most developers there is no way back once they get the hang of it. Sometimes it can get pretty advanced and intimidating though.

This is why I decided to share 5 of my favourite typescript tips and tricks you might have needed in the past. Some are super basic, some are bit more advanced.

Update 07 Okt 2019 @ 07:53
Reddit user jakeboone02 found an error in the ternary code.

Update 06 Okt 2019 @ 15:06
Reddit user smeijer87 found an error in the code for null coalescing.

Update 06 Okt 2019 @ 14:47
A fiendly reader pointed out excluding interface type are called discriminated unions.

  1. react higher-order components
  2. smarter constructor
  3. type checking functions
  4. discriminated unions
  5. optional chaining & null coalescing
Photo by Amador Loureiro on Unsplash

Higher-order Components

In React higher order components (HOC) are very useful tools. Generally they are used to wrap some layout or functionality to some other component. They are simply functions that return another component: basically the same pattern as decorators.

In typescript it can be confusing how to write them maintaining the right props after wrapping the original component. Here you are:

import React from 'react'

function withLayout<P extends object>(WrappedComponent: React.ComponentType<P>) {
  return (props: P) => (
    <div id='app'>
      <Header/>
      <WrappedComponent {...props}/>
      <Footer/>
    </div>
  );
}

Note also that when using the withLayout you don’t need to specify the generic type explicitly, as typescript will inherit from the function parameter. Super handy!

Smarter constructors

Let’s start by the building block this is based on. It’s a basic Javascript trick, not a typescript exclusive at first.

class Pizza {
  slices: number
  name: string
  
  constructor(init) {
    Object.assign(this, init)
  }
}

const pizza = new Pizza({
  slices: 8,
  name: 'Margherita',
})

What is happening here? With the super handy Object.assign simply assigns the object to the class. This is super handy when classes have many constructor parameters. But this is NOT type safe as your IDE/Editor will tell you. How do we fix this?

import { NonFunctionKeys } from 'utility-types'

class Pizza {
  slices!: number
  name?: string

  constructor(init: Pick<Pizza, NonFunctionKeys<Pizza>>) {
    Object.assign(this, init)
  }

  eat() {
    this.slices = 0
  }
}

const pizza = new Pizza({
  slices: 8,
  name: 'Margherita',
})

Let me explain what happens:
This leverages the awesome utility-types package. We first take all the keys that are not a function, so we don’t overwrite the eat method of the class. Then we pick those from the general Pizza type.

This means that slices will be required, while name will be optional, as they are defined.

Type-checking Functions

Did you know you can write functions to tell typescript what type something is? This is awesome!

Suppose we have the following interfaces

interface Food {
  name: string
}

interface Pasta extends Food {
  type: 'Spaghetti' | 'Fusilli'
}

interface Pizza extends Food {
  slices: number
}

Now we could write a cook function that accepts both Pasta and Pizza. Typescript itself cannot differentiate between the too.

function cook(what: Food) {
  if(what === Pizza) ????
}

Fortunately there is a nice solution built into typescript.

function isPizza(x: Food | Pizza): x is Pizza {
  return x.hasOwnProperty('slices')
}

function isPasta(x: Food | Pasta): x is Pasta {
  return x.hasOwnProperty('type')
}


function cook(plate: Food) {
  if (isPizza(plate)) {
    // Plate is now of type Pizza
    putInTheOven(plate)
  }
  if (isPasta(plate)) {
    // Plate is now of type Pasta
    putInThePan(plate)
  }
}

Here we define two functions that return x is Sometype and return a boolean value based on the input. It’s up to you of course to define it properly, but this can be very useful in various situations.

Discriminated unions

type Sqlite = {
  type: 'sqlite',
  database: string,
}

type PostgreSQL = {
  type: 'postgresql',
  database: string,
  host: string,
  post?: number
}

type PossibleConfigs = Sqlite | PostgreSQL

function initialize(config: PossibleConfigs) {}

This might look like a simple one, but I often see people putting those sorts of types all into the same interface. By separating the different type of objects you make sure that they are safe. Also the autocomplete will thank you.

Optional Chaining & Null Coalescing

This are future features that will be introduced in Typescript 3.7 that are very useful and will not be lived without after the release in early November 2019.

Optional chaining is an obvious shorthand. Every time you need to check if a property (especially if nested) exists, you need to do lots of repetitive checking. No more!

a && a.b && a.b.c // 🤬

a?.b?.c // 🚀

Null coalescing is also a very useful shorthand. You all know the || shorthand, often used to initialise a variable if no value is given.

const option = something || 'default'

// Sugar for
const option = !!something ? something : 'default'

The problem is with values that are actual values, but result as falsy.

false || 'default'      // => 'default'
0     || 'default'      // => 'default'

This is where the Null Coalescing comes in.

const option = something ?? 'default'  // 🚀

// Sugar for
const option = (x === null || x === undefined)
  ? 'default'
  : x

0     ?? 'default'      // => 0
false ?? 'default'      // => false

Basically it only assign the default value if the provided one is null or undefined so that values like false or 0 don’t get overwritten.

0.00