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)