Jumpstart on advance typescript Part 2

Jumpstart on advance typescript Part 2

This is continuation of my previous blog where I was trying to introduce some advance Typescript concepts

Type Guards

To better understand this lets look at below code block

const numbers = [0, 1, 2, [3, 4], 5, 6, [7], [8], 9]

function flatten(array: Array<number | Array<number>>): Array<number> {
  const flatten: Array<number> = []
  for (const element of array) {
    if (Array.isArray(element)) {
      flatten.push(...element)
    } else {
      flatten.push(element)
    }
  }
  return flatten
}

flatten(numbers)

In this function we are just trying to flat an array (in this case, return array of numbers) which can have number or array of numbers as element.

on line flatten.push(...element) Typescript allow us to use spread operator inside the if statement without giving any type error. Which means Typescript compiler knows the element inside the if statement will be an array of number because we put a check in if statement which will make sure it should be an array.

Which means Array.isArray() is a type guard in this case and protect us from type error here. Similar way we have other inbuilt type guards: typeof and instanceof

typeof can only be used to check primitive data types: string, number, boolean, function, symbol, object, symbol and undefined types

instanceof can be used to check if an object is an instance of a class. So it means we can use instanceof type guard on class only

But we always don't have simple data structure to deal with in real scenario so how to type guard complex object shape. The answer is create our own type guard!

Custom Type Guard

let create a function to sum all the numbers in array

function calculateSum(array: Array<number>) {
  return array.reduce((sum, curr) => sum + curr, 0)
}

In above function we can only pass an array of numbers as argument

const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
calculateSum(arr) // No TS error

const notFlatArr = [0, 1, 2, [3, 4], 5, 6, [7], [8], 9]
calculateSum(arr) // TS error

To prevent throwing error we will create our own type guard

const notFlatArr = [0, 1, 2, [3, 4], 5, 6, [7], [8], 9]

function isFlat(array: Array<number | Array<number>>): array is Array<number> {
    return !array.some(Array.isArray);
}

if (isFlat(noFlatArr)) {
    calculateSum(arr)
}

Let go one step ahead. Our type guard function is very specific to numbers only. It will be great if we can use it for other types as well. So lets make it generic

function isFlat<T>(array: (T | T[])[]): array is T[] {
    console.log(!array.some(Array.isArray));
}

Now this can be used for any data type and can tell us if the array of any data type is flat or not.

Discriminated Union Types

Discriminated union types allow us to model a finite set of alternative object shapes in the type system. Using it we will only expose valid properties at a given location therefore we will introduce less bugs in system. Lets see some an example for better understanding

type PersonalInfo = {
  name: string
  location: string
}

const personalInfoCollection: { [key: string]: PersonalInfo } = {
  'max@xyz.com': {
    name: 'Max payne',
    location: 'USA',
  },
  'manish@abc.com': {
    name: 'Manish Kumar',
    location: 'India',
  },
}

function getPersonalInfo(email: string) {
  const info = personalInfoCollection[email]
  if (info) {
    return {
      success: true,
      value: info,
    }
  } else {
    return {
      success: false,
      error: 'Information not found',
    }
  }
}

In getPersonalInfo function we are trying to get the info using email address, as you can see the function returns a positive case where we find the information for the email and a negative case where the information is not available for the given email. If we have to give the return type of the function what it could be?

type Result = {
  success: boolean
  value?: PersonalInfo
  error?: string
}

function getPersonalInfo(email: string): Result {
  const info = personalInfoCollection[email]
  if (info) {
    return {
      success: true,
      value: info,
    }
  } else {
    return {
      success: false,
      error: 'Information not found',
    }
  }
}

Type Result will work fine and there will be no typescript error as well as value and error properties and optional, but did you noticed we introduced some run time bug. As value is optional, I can remove it from the positive scenario as below and the function still be type correct but in actual care we want the value if success is true

function getPersonalInfo(email: string): Result {
  const info = personalInfoCollection[email]
  if (info) {
    return {
      success: true,
      // value: info,
    }
  } else {
    return {
      success: false,
      error: 'Information not found',
    }
  }
}

To make sure we return the value when success is true, we can modify our Result Type something like below

type Result =
  | {
      success: true
      value: PersonalInfo
    }
  | {
      success: false
      error: string
    }

Here we have modeled the Return type to reflect specific object structure. Now our function will throw type error if we remove value property because it is not an optional property now. same way error is also not optional.

Every discriminated union types needs a discriminant property to distinguish between the various alternatives. That discriminant property must be of literal type. In our case, we are using the success property as discriminant which is of a Boolean literal type

We can refactor the this code a bit more and make our Result type generic so the it can work with other types as well

type Result<T> =
  | {
      success: true
      value: T
    }
  | {
      success: false
      error: string
    }

function getPersonalInfo(email: string): Result<PersonalInfo> {
  const info = personalInfoCollection[email]
  if (info) {
    return {
      success: true,
      value: info,
    }
  } else {
    return {
      success: false,
      error: 'Information not found',
    }
  }
}

When we use this function interesting things happen, we check if personal information is available by checking success property

const info = getPersonalInfo('manish@kumar.com')
if (info.success) {
  console.log(info.value)
  info.error // TSC Will throw error
} else {
  console.log(info.error)
}

If success is true we will be able to access only value property only, we can use error property here beside Typescript compiler will throw error if we try to do it.

Similarly, in else case we will be able to access only error property.

Using discriminated unions this way can really help you write fewer bugs. The type system forces you to check the discriminant property first before it gives you access to the individual properties. Note that for all of this to work properly, you should have these strict all checks compiler option set to true

That's all for this part. Let me know in comments what you think about it.