DEV Community

SeongKuk Han
SeongKuk Han

Posted on • Updated on

Typescript: Don't use 'as' when using localStorage, define their types

Recently, I have started to do random code reviews and have found some code using localStorage this way.

interface Token {
  accessToken: string;
  refreshToken: string;
}

let token: Token = {
  accessToken: '2YotnFZFEjr1zCsicMWpAA',
  refreshToken: '7b2d6f1e-4a0d-4e1b-a8e8-2e2c9eae1f30',
};

// access
console.log('accessToken', token.accessToken);

// store
localStorage.setItem('token', JSON.stingify(token));

// retrieve
token = JSON.parse(
          localStorage.getItem('token') || '{}'
        ) as Token;

// access
console.log('accessToken', token.accessToken);
Enter fullscreen mode Exit fullscreen mode

I thought there would be a better way to do this.

Converting data using the as keyword every time you retrieve data from localStorage can be a tedious task.

Furthermore, you need to ensure that you use the correct names and types.

For example, if you make a typo for the key, it can lead to a problem.

// ❌ store the data with a key that you don't expect to use
localStorage.setItem('tokennnn', JSON.stingify(token));
Enter fullscreen mode Exit fullscreen mode

If you define functions and types as shown below, you can ensure that data is stored and retrieved with the expected names and types.

interface Token {
  accessToken: string;
  refreshToken: string;
}

type StoreValueFunc<K, V> = (type: K, value: V) => void;

type StoreLevel = StoreValueFunc<'level', number>;
type StoreMessage = StoreValueFunc<'message', string>;
type StoreToken = StoreValueFunc<'token', Token>;

type StoreValue = StoreLevel &
                  StoreMessage &
                  StoreToken;

const storeValue:StoreValue = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value));
}

storeValue('level', 50);

storeValue('message', 'message');

storeValue('token', {
  accessToken: '2YotnFZFEjr1zCsicMWpAA',
  refreshToken: '7b2d6f1e-4a0d-4e1b-a8e8-2e2c9eae1f30',
});

// ❌ type error!
storeValue('level', 'message');

type RetrieveValueFunc<K, V> = (type: K) => V | null;

type RetrieveLevel = RetrieveValueFunc<'level', number>;
type RetrieveMessage = RetrieveValueFunc<'message', string>;
type RetrieveToken = RetrieveValueFunc<'token', Token>;

type RetrieveValue = RetrieveLevel &
                     RetrieveMessage &
                     RetrieveToken;

const retrieveValue: RetrieveValue = (key) => {
  try {
    return JSON.parse(localStorage.getItem(key) || "null");
  } catch {
    return null;
  }
}

// ✅ number | null
const level = retrieveValue('level');

// ✅ string | null
const message = retrieveValue('message');

// ✅ Token | null
const token = retrieveValue('token');

// ✅ access a field of the token
console.log(token?.accessToken);
Enter fullscreen mode Exit fullscreen mode

The retrieveValue function will convert the type from the any type by the key name.

However, it repeats the same names and types, therefore, there is still a possibility that you make a typo and you should synchronize both types by yourself.

StoreValueFunc<'level', number>;
// ...
// ❗️ What if you pass another type instead of number!
RetrieveValueFunc<'level', number>;
Enter fullscreen mode Exit fullscreen mode

To resolve this problem, you can consider defining tuples that have the name and the type.

interface Token {
  accessToken: string;
  refreshToken: string;
}

type LevelType = ['level', number];
type MessageType = ['message', string];
type TokenType = ['token', Token];

type StoreValueFunc<T> = T extends [string, any] ?
                      (key: T[0], value: T[1]) => void :
                      never;

type StoreValue = StoreValueFunc<LevelType> &
                  StoreValueFunc<MessageType> &
                  StoreValueFunc<TokenType>;

const storeValue:StoreValue = (key, value) => {
  localStorage.setItem(key, JSON.stringify(value));
}

storeValue('level', 50);

storeValue('message', 'message');

storeValue('token', {
  accessToken: '2YotnFZFEjr1zCsicMWpAA',
  refreshToken: '7b2d6f1e-4a0d-4e1b-a8e8-2e2c9eae1f30',
});

// ❌ type error!
storeValue('level', 'message');

type RetrieveValueFunc<T> = T extends [string, any] ?
                          (key: T[0]) => T[1] | null :
                          never;

type RetrieveValue = RetrieveValueFunc<LevelType> &
                     RetrieveValueFunc<MessageType> & 
                     RetrieveValueFunc<TokenType>;

const retrieveValue: RetrieveValue = (key) => {
  try {
    return JSON.parse(localStorage.getItem(key) || "null");
  } catch {
    return null;
  }
}

// ✅ number | null
const level = retrieveValue('level');

// ✅ string | null
const message = retrieveValue('message');

// ✅ Token | null
const token = retrieveValue('token');

// ✅ access a field of the token
console.log(token?.accessToken);
Enter fullscreen mode Exit fullscreen mode

The types LevelType, MessageType, and TokenType are used to define the both StoreValue and RetrieveValue types, and you can expect each key uses the same data type when storing and retrieving.

In this example, I directly used the XFunc types, without defining additional types. It's your preference whether to use it or not.


Thank you for reading! 🫡

There may be better ways to solve this problem, if you have some ideas to share with me, please comment below!

Thank you,

Happy Coding!

Top comments (0)