DEV Community

Jack Herrington
Jack Herrington

Posted on • Originally published at Medium on

Mastering Typescript for React Hooks

Mastering TypeScript for React Hooks

So you want to use TypeScript in your React application, but even the hooks are giving you grief. Well, let’s get you comfortable with how to use TypeScript typing with those hooks and get you on your way.

Banner image

This article is meant to supplement the excellent React TypeScript Cheat Sheet which you should definitely take a look at.

useState

useState is a fun one because we use it all the time and most of the time it’s fine, until it’s not. Take this example:

const [myNumber, myNumberSet] = useState(10);

const onClick = () => myNumberSet(20);
Enter fullscreen mode Exit fullscreen mode

TypeScript is totally fine with this because the typing on useState looks at the initial value, sees that it’s a number and sets this type to this:

const [myNumber, myNumberSet] = useState<number>(10);
Enter fullscreen mode Exit fullscreen mode

So any number is going to be fine.

The problem comes up when you have something like this:

const [myAccount, myAccountSet] = useState(null);

const onAuthResponse = () => myAccountSet({ user: "foo", ... });
Enter fullscreen mode Exit fullscreen mode

TypeScript has no idea that what you initially set to null could potentially be an account record. So what you need to do is tell it that:

interface IAccount {
  user: string;
  ...
}
const [myAccount, myAccountSet] = useState<IAccount | null>(null);

const onAuthResponse = () => myAccountSet({ user: "foo", ... });
Enter fullscreen mode Exit fullscreen mode

Now TypeScript understands that your myAccount value can either be null or an object that matches the typing of IAccount.

A similar issue happens with arrays. Take this example:

const [myNumbers, myNumbersSet] = useState([]);

const onClick = () => myNumbersSet([10, 20, 30]);
Enter fullscreen mode Exit fullscreen mode

TypeScript is going to give you a really odd error about trying to use a number[] when a never[] is expected. Which actually makes sense because, as far as TypeScript knows the only valid value is an empty array (i.e. never[]). It has no idea you intend to store numbers in there.

So the fix for this is to type it

const [myNumbers, myNumbersSet] = useState<number[]>([]);

const onClick = () => myNumbersSet([10, 20, 30]);
Enter fullscreen mode Exit fullscreen mode

And now TypeScript will be happy again because even an empty array is a valid type of number[].

useEffect

The great thing about useEffect that that it doesn’t take any types. So if you are looking to make sure you are typing it correctly, fear not, you are.

If you want to check that for yourself then right click on the word useEffect in your VS Code and use the Go to Type Definition command to go to where useEffect is defined in the React source.

useEffect takes two arguments, the first is a function with no parameters that either returns void, or returns another function (the cleanup function), which takes no arguments and returns a void.

IMHO, using Go to Type Definition should be your first stop any time you run into an issue in TypeScript.

useContext

Getting useContext typed properly really comes down to getting the createContext call typed properly. For example, you might have something like this:

const MyContext = createContext(null);
Enter fullscreen mode Exit fullscreen mode

Which basically leaves TypeScript with no clue about what could potentially be in the context and so it leaves it at; the context must always contain null. Which is probably not what you want.

The easiest way to handle this would be, if you want either null or some data, to define it like this:

interface IMyContextState {
  userID: string;
}
const MyContext = createContext<IMyContextState | null>(null);
Enter fullscreen mode Exit fullscreen mode

Which tells TypeScript that the context must either contain an object that matches IMyContextState or null.

If you have a default state it gets a lot easier:

const myDefaultState = {
  userID: "";
}

export type MyContextType = typeof myDefaultState;

const MyContext = createContext(myDefaultState);

export default MyContext;
Enter fullscreen mode Exit fullscreen mode

In this case we don’t need to tell TypeScript that the context has the types in myDefaultState it already knows that, but we now export the schema of the default state as MyContextType. So that we can then use it when we call useContext like so:

import MyContext, { MyContextType } from './store';
...
const ctx:MyContextType = useContext(MyContext);
Enter fullscreen mode Exit fullscreen mode

The typing of ctx is a bit of overkill in this case because useContext already knows the types from MyContext and you can just get away with:

import MyContext from './store';
...
const ctx = useContext(MyContext);
Enter fullscreen mode Exit fullscreen mode

useReducer

Typing useReducer is a lot like typing Redux, so it’s a two-fer, if you get this right, you are that much closer to Redux typing. So useReducer takes two things, the reducer function and the initial state. Let’s start off with the initial state:

const initialState = {
  counter: 0,
};
Enter fullscreen mode Exit fullscreen mode

Next we need some actions. Now in Javascript we wouldn’t type these at all but in TypeScript we can and we should type them, and that would look like this:

type ACTIONTYPES =
  | { type: "increment"; payload: number; }
  | { type: "decrement"; payload: number; };
Enter fullscreen mode Exit fullscreen mode

And then the reducer is going to look something like this:

function myReducer(state: typeof initialState, action: ACTIONTYPES) {
  ...
}

const [state, dispatch] = useReducer(myReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

And this will give you hinting on the state and also ensure that any call to dispatch will need to match one of the variants in ACTIONTYPES.

useRef

Typing useRef, particularly when it comes to use refs with DOM elements, which is a pretty common use case is straightforward. Let’s say you have something like this:

return (<input ref={inputRef} />);
Enter fullscreen mode Exit fullscreen mode

In your code then the corresponding useRef would look like this:

const inputRef = useRef<HTMLInputElement | null>(null);
Enter fullscreen mode Exit fullscreen mode

And specifying the types isn’t 100% necessary here either. The only trick is to make sure you get the right type for the corresponding DOM element.

If you are going to use a ref to hold data then you can do something like this:

const intervalRef = useRef<number | null>(null);
Enter fullscreen mode Exit fullscreen mode

If you are, for example, holding a reference to an interval.

useMemo

The typing on useMemo is all about what is produced by the factory function that you put in there. For example:

const [numbers] = useState([1,2,3,4]);
const filteredNums = useMemo(
  () => numbers.filter(n => n > 2),
  [numbers]
);
Enter fullscreen mode Exit fullscreen mode

In this case the typing on filteredNums is inferred by TypeScript to be number[] because of the output of the factory function. If you wanted to type it you could do:

const filteredNums: number[] = useMemo(
  () => numbers.filter(n => n > 2),
  [numbers]
);
Enter fullscreen mode Exit fullscreen mode

But you really don’t need to. TypeScript is very, very good at figuring out the return type of a function. In fact, if you want to you can use the ReturnType utility type to get the return type from a function like so:

type MyFunctionReturnType = ReturnType<typeof myFunction>;
Enter fullscreen mode Exit fullscreen mode

You can find more information on the amazing array of utility types on the TypeScript language site.

Video Version

If you want to see an in-depth walk through of a lot of this information and much more check out the associated YouTube video:

Conclusions

The more I work with TypeScript and React the more that I am convinced that it’s worth the investment. You get the benefits of hinting as you code. You are communicating your intent through the types. And you get the benefits of a type safety check at compile time.

Hopefully this article will help you realize these benefits as you try out using TypeScript in your React projects and you learn to master the typing of your React hooks.


Discussion (0)