Home > OS >  Linting errors in react testing
Linting errors in react testing

Time:07-06

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.


See playground

  • Related