Lessons learned from using Typescript Part #1

June 16, 2021 / 5 min read

(Even though I learned about C# and C++ all these strongly typed languages at school, typescript honestly gave me a bit friendly vibe similar to C#).

Consider the following function:

javascript
const getFromStorage = async() => {
try {
const storageKey = getStorageKey()
return await appStorage.getItem(storageKey)
} catch (error) {
// handle error
}
}

The getFromStorage function retrieves data from web browser storage (indexedDB, local storage etc.), and returns the data. simple enough. Now let's add typescript to the mix!

typescript
const getFromStorage = async <T>() => {
try {
const storageKey = getStorageKey()
return await appStorage.getItem<T>(storageKey)
} catch (error) {
// handle error
}
}

In the above code snippet, I’ve added <T> and appStorage.getItem<T>. Don’t worry if you do now know what is it yet, I’m going to explain a little bit about typescripts generics in my own term, but you can also check out the official documentation

Typescript generics

Think of generics as one superset of types, where is a black box and you feed different types into that magic box, or I would like to call it reusable box (modern-day software architectures without code or component reusability won’t scale and won’t be sustainable). For now, can’t think of anything else other than this analogy, so stick with me here.

Back to our previous example, <T> just means is a Type parameters, and we can supply any kind of type we want when we call this function. Next line appStorage.getItem<T> is reduced here, but the longer version of this is that appStorage is an object which has getItem property function where it will returns null or the passed in Type, in this case it’s generic.

If we explicitly type this function return value, we can do it like this:

typescript
const getFromStorage = async <T>(): Promise<T | null> => {
try {
const storageKey = getStorageKey()
return await appStorage.getItem<T>(storageKey)
} catch (error) {
// handle error
}
}

Even though getItem returns the same thing, but we can also do it this way. Sometimes it can be useful for other developers, since most of the time all we do is reading other developer’s code.

Define DataType

Let’s imagine we have some data stored in the localStorage and we called it DataType, and here it’s what it looks like.

typescript
type DataType = {
firstName: string
lastName: string
}

Hmm, ok something is missing, what if our data type is some nullish value in some case? Like for example if user accidentally deletes it and getItem function returns null instead of the actual value itself?

You thinking what I’m thinking? Yep, just adds a null type in the end.

typescript
type DataType = {
firstName: string
lastName: string
} | null

Voila, problem solved! Not quite!! But this is why I wrote his blog post in the first place. Stay and let me explain as much as I can.

Attention to the details

This actually can be a good interview question for junior devs honestly, so before move on to the answer, tell me what data type should be.

typescript
const getFromStorage = async <T>() => {
try {
const storageKey = getStorageKey()
// getItem returns Promise<T | null>
return await appStorage.getItem<T>(storageKey)
} catch (error) {
// handle error
}
}
const doSomethingWithDataFromStorage = async () => {
try {
// what's of type data?
const data = await getFromStorage<DataType>()
} catch (error) {
// error
}
}

If you thinking DataType | null then congrats, you made the same mistake I made! And this was a really good learning experience for me.

The correct answer is that data will be of type Promise<DataType>, you won’t get the null type. Before I dig deeper, note that the reason nullis so important here is because we want typescript to catch the null type before we do something with it so that it won’t break on run time. data.firstName is undefined, and our app crashes, and you have to fix it later! This is where typescript shines and javascript does not!

typescript
const doSomethingWithDataFromStorage = async () => {
try {
// data is of type Promise<DataType | null>
const data = await getFromStorage<DataType>()
// so we can check for null value here
// if (!data) {
// do something if can't find data
// }
// oho! this is gonna crash!
if (data.firstName === 'John') {
// do something here
}
} catch (error) {
// error
}
}

The conclusion

So how do we fix it?

Easy, just remove the null from the DataType, let typescript refer the types from getItem .

typescript
type DataType = {
firstName: string
lastName: string
}
typescript
const doSomethingWithDataFromStorage = async () => {
try {
// data is now of type DataType | null
const data = await getFromStorage<DataType>()
if (!data) {
// do something if data is null
}
} catch (error) {
// error
}
}

Typescript is very smart, it refers types even if you don't specify them in the first place. It let's you catches bug before it happens, and also has many positive impact on working with other developers. Since this is a one of my #shorts post, I'm going to leave it here, and come back another time for another post about typescript. Be sure to stay tuned till then!