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);
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));
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);
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>;
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);
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)