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.
Top comments (0)