How do we type useRef()?
Chapter 3: TypeScript for real-world applications
Chapters - Table of contents
As we saw in the Chapter introduction, depending on the function argument, useRef() has different return types:
1const mutableRef = useRef()
2// `mutableRef` is of type `MutableRefObject`
3
4const immutableRef = useRef(null)
5// `immutableRef` is of type `RefObject`
6
TypeScript allows us to define the different signatures of a given function using Function overloads:
The answer is to supply multiple function types for the same function as a list of overloads. This list is what the compiler will use to resolve function calls.

Function overloads overview

Let's explain function overloads with a more straightforward use-case than useRef(). Remember our sum(a, b) function of Chapter 1?
1const sum = (a, b) => a + b;
2
3sum(1, "2") // "12" -> ??!?!?!
4
Instead of changing the function behavior, we could choose to change the function typing to inform its user of possible side-effects:
1sum(1, 1) // => 2
2
3sum(1, "1") // => "11"
4
To achieve this, we need to inform TypeScript which types are expected given every scenario, which are:
  • function sum (a: number, b: number): number ➡️sum(1, 1) // => 1
  • function sum (a: string, b: number): string ➡️sum('1', 1) // => '11'
  • function sum (a: number, b: string): string ➡️sum(1, '1') // => '11'
  • function sum (a: string, b: string): string ➡️sum('1', '1') // => '11'
Describing all the possible typings to TypeScript is straightforward; We have to add type definitions, without bodies, on top of the function implementation (with a body), as follows:
1function sum (a: number, b: number): number; // sum(1, 1) // => 1
2function sum (a: string, b: number): string; // sum('1', 1) // => '11'
3function sum (a: number, b: string): string; // sum('1', 1) // => '11'
4function sum (a: string, b: string): string; // sum('1', '1') // => '11'
5function sum (a: string | number, b: string | number): string | number {
6  // ...
7}
8
The tricky part lies in the function implementation, which should allow all defined overloads to fit. Some examples:
1function sum (a: number, b: number): number; // overload 1
2function sum (a: string, b: number): string; // overload 2
3function sum (a: number, b: string): string; // overload 3
4function sum (a: string, b: string): string; // overload 4
5// ERROR!
6// the function implementation does not match
7//  the overloads 1, 2, 3 
8function sum (a: string, b: string): string {
9  // ...
10}
11
12
13function sum (a: number, b: number): number;
14function sum (a: string, b: number): string;
15function sum (a: number, b: string): string;
16function sum (a: string, b: string): string;
17// ERROR!
18// the function implementation does not match
19//  the overloads 1 and 3
20function sum (a: string, b: string | number): string | number {
21  // ...
22}
23
To conclude:
  • overloads should describe specific use-cases
  • the function implementation should represent all possible use-cases

Implement useRef() overloads

Let's now apply function overloads to the useRef() React hook. As a reminder:
  • when no argument is passed, the returned ref is mutable
  • otherwise, the returned ref is immutable
Given the following return types:
1// an immutable `ref` object
2interface RefObject {
3  readonly current: any
4}
5
6// a mutable `ref` object
7interface MutableRefObject {
8  current: any
9}
10
What is this any type?
In TypeScript, the lack of typing is expressed with the any type, which basically means "can contain any type of value".
When no types can be assigned to a variable, argument, or properties, we use the any type. Also, when TypeScript cannot infer a variable, argument, or properties type, it assigns it the any type.
We will come back to the any type in Chapter 4.
Our overloads would be:
1function useRef(initialValue: any): RefObject;
2function useRef(): MutableRefObject;
3
with the following useRef() implementation:
1// `ref` can be undefined (2nd overload)
2// `useRef()` can either return a `RefObject` or `MutableRefObject`
3function useRef(ref?: any): RefObject | MutableRefObject {
4  // ...
5}
6

The interface based version

As we saw in the last chapter, - anonymous - functions can also be typed using interface, which, in the case of overload leads to a clearer syntax:
1interface RefObject {
2  readonly current: any
3}
4
5interface MutableRefObject {
6  current: any
7}
8
9interface UseRef {
10  (initialValue: any): RefObject;
11  (): MutableRefObject;
12}
13
14const useRef: UseRef = (ref?: any): MutableRefObject | RefObject => {
15  return {
16    current: null
17  }
18}
19
20const mutableRef = useRef()
21// `mutableRef` is of type `MutableRefObject`
22
23const immutableRef = useRef(null)
24// `immutableRef` is of type `RefObject`
25

When should we use function overloads?

The following _rule of thumb _should stay on top of your mind when typing functions:
Always prefer parameters with union types instead of overloads when possible
In short, when typing a function or components, always try to achieve it by using _union type _for arguments. If it's not possible, you might need to write some overloads.

Up next

We will see more advanced examples of function overload in the "Building re-usable types" page. __Let's move forward by building types for useState() using TypeScript generics.
We use cookies to collect statistics through Google Analytics.
Do not track
 
Allow cookies