DEV Community

Cover image for Fix React.useState using "as const"
Cibi Aananth
Cibi Aananth

Posted on • Originally published at blog.asciibi.dev

Fix React.useState using "as const"

Let's take a typical React Component that fetches data from an external source and updates the UI based on the network state

// Users.tsx
import { useState, useEffect } from "react";

export const REQUEST_STATUS = {
  idle: "IDLE",
  pending: "PENDING",
  success: "SUCCESS",
  error: "ERROR",
};

async function fetchUsers(): Promise<{ name: string; id: number }[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([{ name: "User 1", id: 1 }]);
    }, 2000);
  });
}

export function App() {
  const [networkState, setNetworkState] = useState(REQUEST_STATUS.idle);
  const [users, setUsers] = useState<{ name: string; id: number }[]>([]);

  useEffect(() => {
    setNetworkState(REQUEST_STATUS.pending);
    // or setNetworkState("IDLE"); // valid
    const getUsers = async () => {
      const users = await fetchUsers();
      setUsers(users);
      setNetworkState(REQUEST_STATUS.success);
    };
    getUsers();
  }, []);

  return (
    <div>
      {networkState === "PENDING" ? (
        <p>Fetching data...</p>
      ) : (
        networkState === "SUCCESS" && (
          <li>
            {users?.map((user) => (
              <ul key={user.id}> {user.name} </ul>
            ))}
          </li>
        )
      )}
      {networkState === "ERROR" && (
        <div>An error occured while fetching users</div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this code may seem perfectly fine and in fact, this will render the component without any error.

BUT...

Let's assume that the REQUEST_STATUS is imported from some global file and used in our components like,

import { REQUEST_STATUS } from '../some/other/file'
Enter fullscreen mode Exit fullscreen mode

and someone changed the statuses in the global file to something that breaks our Users.tsx component. Boom!

export const REQUEST_STATUS = {
  idle: "idle",
  pending: "pending",
  success: "success",
  error: "error",
};
Enter fullscreen mode Exit fullscreen mode

Typechecking in useState

While adding a type to the setNetworkState is the ultimate fix to this problem, how do we do it without much change in the code?

First, let's infer the type of REQUEST_STATUS in our User component,

Image description

Here the type of the keys idle, pending, success, error is inferred as a string therefore using keyof and typeof would still result in string as the overall type.

Image description

Detour - keyof typeof

If the above syntax is confusing, let me break it down for you ;) Otherwise, skip to the next section.

typeof

Here TypeScript is going to be able to infer the type of your object for you. It's going to lift this object that you created from the value level to the type level.

so essentially typeof REQUEST_STATUS will be inferred as { idle: string, pending: string, success: string, error: string }

So if we want to get a type of particular key in the object, we can do something like

type IDLE_TYPE = typeof REQUEST_STATUS["idle"]; // string
type PENDING_TYPE = typeof REQUEST_STATUS["pending"]; // string
type SUCCESS_TYPE = typeof REQUEST_STATUS["success"]; // string
type ERROR_TYPE = typeof REQUEST_STATUS["error"]; // string
Enter fullscreen mode Exit fullscreen mode

keyof

The keyof operator can be used to extract the keys of an object type.

Image description

keyof can be used in conjunction with typeof to create a union type of all keys in the object.

By extracting all the keys of the object type, we can combine these keywords to create union types

type REQUEST_TYPE = tyepof REQUEST_STATUS[keyof typeof REQUEST_STATUS] // string

// the above expression is equivalent to
type REQUEST_TYPE =
| { idle: string, pending: string, success: string, error: string }["idle"] 
| { idle: string, pending: string, success: string, error: string }["pending"]
| { idle: string, pending: string, success: string, error: string }["success"]
| { idle: string, pending: string, success: string, error: string }["error"]
Enter fullscreen mode Exit fullscreen mode

Back to rescue with "as const"

Now that we know how typeof and keyof can be used together to create union type from objects, let's see how "as const" fits in here.

The above type export type REQUEST_TYPE = tyepof REQUEST_STATUS[keyof typeof REQUEST_STATUS] returns string as the type, so if typing our setState wouldn't solve our problem. This is because as long as we set a "string", TypeScript accepts it as a valid value.

import { REQUEST_TYPE, REQUEST_STATUS } from '../some/other/file'

const [networkState, setNetworkState] = useState<REQUEST_TYPE>(REQUEST_STATES.idle);

// ...somewhere in the code, we can still do
setNetworkState("idle"); // no error
setNetworkState("invalid_state"); // no error
setNetworkState(12) // Error as type "string" is inferred from initial state
Enter fullscreen mode Exit fullscreen mode

Without further explanation, this is where "as const" assertion comes into play and makes it strongly typed. By simply adding as const to the object we can make it as readonly and ensure that all properties are assigned the literal type instead of a more general version like string or number

Image description

Combining with our previously learned trick we can create a union type of object values

Image description

Putting it together

// types
export const REQUEST_STATUS = {
  idle: "IDLE",
  pending: "PENDING",
  success: "SUCCESS",
  error: "ERROR",
} as const;

export type REQUEST_TYPES = typeof REQUEST_STATUS[keyof typeof REQUEST_STATUS];

// User.tsx
const [networkState, setNetworkState] = useState<REQUEST_TYPES>(REQUEST_STATUS.idle);
Enter fullscreen mode Exit fullscreen mode

With this updated useState, TypeScript compiler will throw an error for invalid statuses.

Image description

Image description

Conclusion

In conclusion, using "as const" in conjunction with typeof and keyof can help you create strongly typed union types for your React useState hook, preventing potential bugs caused by unexpected value changes in your application. By implementing this approach, you'll ensure better type safety and maintainability in your codebase.

Happy coding!

Top comments (1)

Collapse
 
bhendi profile image
Jyothikrishna

Great article @cibi