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.
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);
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);
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", ... });
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", ... });
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]);
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]);
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);
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);
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;
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);
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);
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,
};
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; };
And then the reducer is going to look something like this:
function myReducer(state: typeof initialState, action: ACTIONTYPES) {
  ...
}
const [state, dispatch] = useReducer(myReducer, initialState);
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} />);
In your code then the corresponding useRef would look like this:
const inputRef = useRef<HTMLInputElement | null>(null);
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);
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]
);
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]
);
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>;
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.
 


 
    
Top comments (0)