« Home

Using Typescript to force errors handling

Summary: Typescript’s discriminated unions may be used to force you to handle exceptions.

The problem

If a function can throw an error, it should be wrapped in try/catch block to be properly handled.

However, error handling can be ignored, which adds more room for potential errors.

function getUserFromStorage(id: string) {
  return undefined
}

function getUser(id: string) {
  const user = getUserFromStorage(id)
  if (user === undefined)
    throw new Error('Specified user does not exist')

  return user
}

function printUserInfo(id: string) {
  const user = getUser(id)

  console.log(`User name is ${user.name}`)
}

The problem here is that we didn’t check for errors when we retrieve user from our storage. Moreover, TS compiler don’t actually care.

Ideally, printUserInfo should handle possible errors:

function printUserInfo(id: string) {
  try {
    const user = getUser(id)

    console.log(`User name is ${user.name}`)
  }
  catch {
    console.error('Some error')
  }
}

Discriminated unions

A union type is a type that combines multiple types into one. A variable of a union type can store only values with those types:

type mode = 'edit' | 'view' | number

// ok, type ='edit'
const a = 'edit'

// ok, type = number
const b = 24

// ok, type = 'view'
const c = 'view'

// type error: type string is unassignable to type mode
const d = 'randomstring'

Union types and type narrowing

When dealing with union types, we often need to narrow them before use:

type StringOrNumber = string | number

function getStringOrNumber(): StringOrNumber {
    return 2
}

let v = getStringOrNumber()

// Error!
// The left-hand side of an arithmetic operation must be of 
// type 'any', 'number', 'bigint' or an enum type.
console.log(v ** 3)

See, typescript complains because it doesn’t know whether v is string or number, Therefore, we need to make sure that v is a number:

type StringOrNumber = string | number

function getStringOrNumber(): StringOrNumber {
    return 2
}
let v = getStringOrNumber()

if (typeof v === 'number')
    console.log(v ** 3)
else
    console.log(v.toLowerCase())

Take a look at the last example one more time; did you notice console.log(v.toLowerCase())?

It works because this statement is placed in the else branch of our check, so typescript knows that v is a string.

Fixing the problem

We can use union types and required type narrowing to make result checking mandatory - or our program won’t be compiled.

This is how it can be implemenented.

First of all, we create two main types and their union:

interface OkResult<T> {
  ok: true
  value: T
}

interface FailResult {
  ok: false
  error: string
}

type OkOrFail<T> = OkResult<T> | FailResult

Each of these types has ok field, which we can use to narrow OkOrFail type to OkResult or FailResult:

type User = {
    name: string
    email: string
}

function getUser(id: number): OkOrFail<User> {
  try {
    const user = getUserFromDb(id)

    return {
      ok: true,
      value: user,
    }
  }
  catch {
    return {
      ok: false,
      error: 'can\'t find a user'
    }
  }
}

We used try/catch to return OkResult or FailResult, so our function doesn’t throw errors.

It returns union type instead, and in order to work with the returned error we need to narrow result to OkResult type:

const u = getUser(1)

// Type Error!
// If u is `FailResult`, it doesn't have the `value` field!
console.log(u.value)

// Now everything is ok, we narrow result down to `OkResult` type
if (u.ok)
  console.log(u.value)

Always returning literal objects is too much to type, so we can create helper functions for this:

function ok<T>(value: T): OkResult<T> {
  return {
    ok: true,
    value,
  }
}

function fail(error: string): FailResult {
  return {
    ok: false,
    error,
  }
}

And then our getUser can be refactored:

function getUser(id: number): OkOrFail<User> {
  try {
    const user = getUserFromDb(id)

    return ok(user)
  }
  catch {
    return fail('can\'t find a user')
  }
}

That’s it, I hope you liked this article, good luck!