I am running some tests on a custom hook and the tests are working fine, although I'm getting a lot of errors under my localStorage, TEST_VALUE. Under localStorage I receive the following:
Argument of type 'string | null' is not assignable to parameter of type 'string'.
Type 'null' is not assignable to type 'string'.ts(2345)
I'm sure it is coming from my hook:
import { useState, useEffect } from 'react';
export const useStateWithLocalStorage = (defaultValue: string, key: string) => {
const [value, setValue] = useState(() => {
const storedValues = localStorage.getItem(key);
return storedValues !== null ? JSON.parse(storedValues) : defaultValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
But don't know how I would get around it as !== null needs to be in the return for the function to work.
Here is my test file:
import { renderHook, act } from '@testing-library/react';
import { useStateWithLocalStorage } from '../UseStateWithLocalStorage';
describe('Test local storage', () => {
test('should set local storage with default value', () => {
const TEST_KEY: string = 'form';
const TEST_VALUE = { name: 'matt' };
renderHook(() => useStateWithLocalStorage(TEST_VALUE, TEST_KEY));
//^^ ERROR HERE UNDER TEST_VALUE Argument of type '{ name: string; }' is not assignable to parameter of type 'string'.
expect(JSON.parse(localStorage.getItem(TEST_KEY))).toEqual(TEST_VALUE);
});
//^^ERROR HERE UNDER (localStorage.getItem(TEST_KEY) Argument of type 'string | null' is not assignable to parameter of type 'string'. Type 'null' is not assignable to type 'string'.ts
test('should update localStorage when state changes', () => {
const TEST_KEY: string | null = 'form';
const TEST_VALUE = { name: 'bill' };
const { result } = renderHook(() => useStateWithLocalStorage(TEST_VALUE, TEST_KEY));
// Pulls from result.current to update state
const [, setValue] = result.current;
const newValue = { name: 'john' };
act(() => {
setValue(newValue);
});
expect(JSON.parse(localStorage.getItem(TEST_KEY))).toEqual(newValue);
});
});
Am I missing a type somewhere?
CodePudding user response:
Second error
The second one is easier, so lets start there.
localStorage.getItem
is typed as:
Storage.getItem(key: string): string | null
Which means it accepts a string
argument, but you pass it string | null
const TEST_KEY: string | null = 'form';
JSON.parse(localStorage.getItem(TEST_KEY))
That's what this error means:
Argument of type 'string | null' is not assignable to parameter of type 'string'.
Type 'null' is not assignable to type 'string'.(2345)
In this case you want to remove the type annotation for TEST_KEY
which will make it typed as just string
.
But then getItem
returns string | null
and JSON.parse
accepts a string
and can't handle null
. So you have a similar problem.
You need to guard against that, too. But given that this is just a test, you don't have to provide a valid JSON string, since it will probably never even execute.
This means you could rewrite the above snippet to something like:
const TEST_KEY = 'form';
JSON.parse(localStorage.getItem(TEST_KEY) ?? 'error')
Or could also use a postfix !
to assert that the value is non null, but I don't recommend you use that because it's a bad habit.
const TEST_KEY = 'form';
JSON.parse(localStorage.getItem(TEST_KEY)!)
First error
You then type the first argument of your hook as a string
in:
export const useStateWithLocalStorage = (defaultValue: string, key: string) => {
Then you pass it an object:
const TEST_VALUE = { name: 'matt' };
renderHook(() => useStateWithLocalStorage(TEST_VALUE, TEST_KEY));
That's what this error means:
Argument of type '{ name: string; }' is not assignable to parameter of type 'string'.(2345)
To fix that, you must type useStateWithLocalStorage
correctly. It seems you expect to able to pass complex objects to useStateWithLocalStorage
not just strings, so we need to change the type of the function argument.
But also, you want strong type safety for whatever the state type is. You could make that argument type any
or unknown
, but then you don't have strong type safety between input and output. So to do this right, I believe you need to make the function generic.
export const useStateWithLocalStorage = <T>(defaultValue: T, key: string) => {
const [value, setValue] = useState<T>(() => {
const storedValues = localStorage.getItem(key);
return storedValues !== null ? JSON.parse(storedValues) : defaultValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Now defaultValue
can be any unknown type, which is remember as T
. We then pass that type to useState
to make that state the same type as defaultValue
. And now your function will return [T, Dispatch<SetStateAction<T>>]
which gives you a strongly typed return value.