Typescript: Don't use 'as' when using localStorage, define their types
Tags: typescript, webdev, frontend, javascript
Published: 2023-09-17 16:24:27

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

```typescript
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.

```typescript
// ❌ 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.

```typescript
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.

```typescript
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.

```typescript
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!