useEffect.dev
← Back to lessons

Lesson 14. Better hooks with TypeScript 4. Typing custom hooks

Typing a custom hook is not different than typing any function: you have to care about giving types to the parameters, and to the returned value if necessary (or if you prefer to, so the code is easier to read).

Additionnally, if you give types to React hooks inside your custom hooks, you shoudln’t have to explicitely type a lot more.

interface Planet {
name: string;
}
const usePlanet = (id: string) => {
const [loading, setLoading] = useState(false)
const [planet, setPlanet] = (useState < Planet) | (null > null)
useEffect(() => {
setLoading(true)
// Fetch the planet…
}, [id])
return { loading, planet }
}

In this example, we gave the string type to the hook parameter id, and boolean and Planet | null to our local state inside this custom hook. This is all we have to do: TypeScript is able to infer that the hook will return an object with two attributes: { loading: boolean, planet: Planet | null }.

Most of the time, it won’t more difficult to type your custom hooks. But let’s consider this other example, which offers a way to create a state and perform some action when it changes:

const useStateWithActionWhenChanged = (initialValue, onChange) => {
const [state, setState] = useState(initialValue)
useEffect(onChange, [state])
return [state, setState]
}

This custom hook has no interest in the real world, since you can’t even access the current state’s value in the onChange callback you pass as parameter to the hook. But what is interesting with it is how to give it a type.

Ideally, we want the user to be able to store any value in the local state, so let’s use TypeScript’s generics, and imagine that the state is of type StateType:

const [state, setState] = useState<StateType>(initialValue)

This means that if we want to allow initialValue to be the same type as what useState expects, it has to be of type StateType | (() => StateType) (remember that useState also accepts a callback as parameter, see lesson 1).

Now, what is onChange’s type? It has to be the exact type expected by useEffect for its first parameter, which is React.EffectCallback.

Finally, what does this hook return? If we let TypeScript infer the hook return type, it will be:

(StateType | React.Dispatch<React.SetStateAction<StateType>>)[]

Indeed, the inferred type for an array [a, b], where a and b are respectively of types A and B, is (A | B)[]. Here, we’d prefer having [A, B], wouldn’t we?

Two solutions for that:

  • we can either give an explicit return type to our hook:
[StateType, React.Dispatch<React.SetStateAction<StateType>>]
  • or we can add as const after the returned value:
return [state, setState] as const

Choose the option you prefer. I personnaly always prefer a solution involving as few explicit types as possible.

Here is the final types implementation of our hook:

const useStateWithActionWhenChanged = <StateType extends {}>(
initialValue: StateType | (() => StateType),
onChange: React.EffectCallback
) => {
const [state, setState] = useState<StateType>(initialValue)
useEffect(onChange, [state])
return [state, setState] as const
}

Notice that to use generics we write <StateType extends {}> instead of just <StateType>. Both syntaxes are strictly equivalent, but writing <StateType> gives a syntax error when using TypeScript with JSX (the compiler thinks <StateType> is a JSX element). Writing extends {} is a small trick to prevent this.