React components and hooks
Chapter 2: Getting started with TypeScript
Chapters - Table of contents

Foreword: React and TypeScript

This training only covers modern React applications using functional components.

TypeScript and JSX

When it comes to typing JSX elements, TypeScript and React have a deal:
  • For intrinsic elements (ex: <div>), React must provide props typings for all elements in a JSX.IntrinsicElements interface (see here). All intrinsic elements must start with a lowercase letter.
<div> is an intrinsic element which types are provided by React.
is an intrinsic element which types are provided by React.
  • For custom components (cf: value-based elements), TypeScript is responsible for trying to "guess" whenever this element is a function or a class and infer its props types.
In a JSX context, TS will infer Button as a function along with its prop typings.
In a JSX context, TS will infer Button as a function along with its prop typings.

Typing functions

Typing functions in TypeScript is achieved as follows:
1// functional component
2function MyComponent(props: MyComponentProps): JSX.Element { /* ... */ }
3
4// functional component (as a anonymous function)
5const MyComponent = (props: MyComponentProps): JSX.Element => { /* ... */ }
6
  • The function argument has type suffix: : MyComponentProps
  • The function signature also has a type suffix: : JSX.Element

Typing the <Button /> component

We already defined our <Button /> props types are as follows
1interface ThemableComponentProps {
2  type: "primary" | "secondary";
3  size: "small" | "medium" | "large";
4  classNames: string[];
5}
6
7interface FormComponentProps {
8  name: string;
9  label?: string;
10  disabled?: boolean;
11}
12
13interface ButtonProps extends ThemableComponentProps, FormComponentProps {
14  loading?: boolean; // adding a loading state to `<Button />`
15}
16
Let's now assign it to our <Button /> component:
1interface ThemableComponentProps {
2  type: "primary" | "secondary";
3  size: "small" | "medium" | "large";
4  classNames: string[];
5}
6
7interface FormComponentProps {
8  name: string;
9  label?: string;
10  disabled?: boolean;
11}
12
13interface ButtonProps extends ThemableComponentProps, FormComponentProps {
14  loading?: boolean; // adding a loading state to `<Button />`
15}
16
17const Button = (props: ButtonProps): JSX.Element => {
18  const className = [
19    "button",
20    props.disabled ? 'button__disabled' : undefined,
21    `button__size-${props.size}`,
22    `button__type-${props.type}`,
23    props.classNames].filter(c => !!c).join(" ")
24  return (
25    <div className={className} id={`button-${props.name}`}>
26      <div className={"button--label"}>
27        {props.loading ? "Loading ..." : props.label || "Continue"}
28      </div>
29    </div>
30  );
31};
32
As discussed in the introduction, TypeScript is expecting functional components to provide, optionally, a first argument that would be the component's props. The JSX.Element type, exposed by React, defines a valid JSX element type.

Object destructuring for arguments

Most applications leverage object destructuring on props for a clearer code, as follows:
1interface ThemableComponentProps {
2  type: "primary" | "secondary";
3  size: "small" | "medium" | "large";
4  classNames: string[];
5}
6
7interface FormComponentProps {
8  name: string;
9  label?: string;
10  disabled?: boolean;
11}
12
13interface ButtonProps extends ThemableComponentProps, FormComponentProps {
14  loading?: boolean; // adding a loading state to `<Button />`
15}
16
17const Button = ({
18  classNames,
19  name,
20  size,
21  type,
22  label,
23  loading,
24  disabled,
25}: ButtonProps) => {
26  const className = [
27    "button",
28    disabled ? "button__disabled" : undefined,
29    `button__size-${size}`,
30    `button__type-${type}`,
31    classNames,
32  ]
33    .filter((c) => !!c)
34    .join(" ");
35  return (
36    <div className={className} id={`button-${name}`}>
37      <div className={"button--label"}>
38        {loading ? "Loading ..." : label || "Continue"}
39      </div>
40    </div>
41  );
42};
43
Why our <Button> is now missing a JSX.Element return type?
As you might have noticed, our last refactoring also dropped the return type of the component. This has been done to illustrate the following TypeScript rule:
"As the name suggests [talking about functional component], the component is defined as a JavaScript function where its first argument is a props object. TS enforces that its return type must be assignable to JSX.Element." https://www.typescriptlang.org/docs/handbook/jsx.html#function-component
Meaning that, when it comes to functional components, you don't need to explicitly specify a return type since TypeScript will assume that it should resolve to JSX.Element.

Typing a custom hook

Modern React applications mostly rely on React core hooks (useState()) and custom hooks. Custom React hooks are functions that follow 2 very simple rules - on top of the rules of hooks:
  • A function is a custom hook if it relies on other hooks (ex: useState(), useMemo())
  • A custom hook should follow the naming convention: use[HookName]()
Since custom hooks are function, typing them is straightforward, consider the following useCurrentUserName() custom hook:
1const useCurrentUserName = ({ format }: { format: 'short' | 'long' }): string => {
2  // get current user object from a context
3  const user = useContext(CurrentUserContext)
4
5  // format user's name and return it...
6}
7
8// ...
9
10const userName = useCurrentUserName({ format: 'short' }) // => "Charly P."
11
Inline argument typings
You might have noticed in the example above, that the object argument types are inlined:
({ format }: { format: 'short' | 'long' })
This is a common practice when types are simple and short, avoiding the overhead of building a separate interface. However, beware of using it along with object destructuring since it can become confusing:
({ format: formatValue }: { format: 'short' | 'long' })
Here, format: formatValue is a destructuring assignment to a local variable called formatValue.
We can improve our useCurrentUserName() hook by adding a default format value as follows:
1const useCurrentUserName = (
2   { format }: { format: 'short' | 'long' } = { format: 'short' } 
3): string => {
4  // get current user object from a context
5  const user = useContext(CurrentUserContext)
6
7  // format user's name and return it...
8}
9
When using a default value, no ? type prefix is necessary. TypeScript automatically infers that the type can be undefined which is equivalent to the following type:
1const useCurrentUserName = (
2   { format }: { format: 'short' | 'long' } = { format: 'short' } 
3): string => { /* ... */ }
4
5// translates to
6
7const useCurrentUserName = (
8   { format }?: { format: "short" | "long"; }
9): string => { /* ... */ }
10

Functions as object properties

Back to our <Button /> component, which could not really be a real-world component without a classic onClick props.
Let's add a onClick props to the ButtonProps type:
1interface ButtonProps extends ThemableComponentProps, FormComponentProps {
2  loading?: boolean;
3  // our onClick callback props
4  onClick: () => void
5  //          ^ not ":" but "=>"
6}
7
Typing function on variable or object properties is very similar to typing functions; However, beware, the return type is declared using the => operator.
Let's see a comprehensive example with a custom useToggle() hook:
1import { useState, useCallback } from 'react'
2
3interface UseToggleReturnValue {
4  value: boolean
5  setToFalse: () => void
6  setToTrue: () => void
7  toggle: () => void
8}
9
10const useToggle = (defaultValue = false): UseToggleReturnValue => {
11  const [value, setValue] = useState(defaultValue)
12
13  return {
14    setToFalse: useCallback(
15      () => { setValue(false) },
16      [setValue]
17    ),
18    setToTrue: useCallback(
19      () => { setValue(true) },
20      [setValue]
21    ),
22    toggle: useCallback(
23      () => { setValue(!value) },
24      [setValue, value]
25    ),
26    value
27  }
28}
29
30// Usage:
31const {
32  value: modalVisible,
33  setToFalse: closeModal,
34  toggle: toggleModal
35} = useToggle(true)
36
37toggleModal() // value => false
38

Side note: Type functions with interface

So far, we have learned how to provide types to functions by annotating the functions directly. However, a cleaner alternative is possible by leveraging the interface keyword as it follows:
1import { useState, useCallback } from 'react'
2
3interface UseToggle {
4  // note, here the function return type is
5  //   prefixed with a ":" 
6  (defaultValue?: boolean): {
7    value: boolean
8    // as property, a function return type is 
9    //  prefixed with a "=>" 
10    setToFalse: () => void
11    setToTrue: () => void
12    toggle: () => void
13  }
14}
15
16const useToggle: UseToggle = (defaultValue = false) => {
17  const [value, setValue] = useState(defaultValue)
18
19  return {
20    setToFalse: useCallback(
21      () => { setValue(false) },
22      [setValue]
23    ),
24    setToTrue: useCallback(
25      () => { setValue(true) },
26      [setValue]
27    ),
28    toggle: useCallback(
29      () => { setValue(!value) },
30      [setValue, value]
31    ),
32    value
33  }
34}
35
By adding a () at the root of the interface, we provide a callable signature.
Using interface to type functions is only possible for anonymous functions, assigned to a variable.
We use cookies to collect statistics through Google Analytics.
Do not track
 
Allow cookies