(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:
javascriptconst 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!
typescriptconst 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:
typescriptconst 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.
typescripttype DataType = {firstName: stringlastName: 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.
typescripttype DataType = {firstName: stringlastName: 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.
typescriptconst 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 null
is 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!
typescriptconst 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
.
typescripttype DataType = {firstName: stringlastName: string}
typescriptconst doSomethingWithDataFromStorage = async () => {try {// data is now of type DataType | nullconst 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!