Adopting TypeScript for React developers: how?

By adopting TypeScript, React developers take advantage of the language's type safety, code analysis, and refactoring capabilities. In this article, we'll explore the steps needed to adopt TypeScript.

Prerequisites:

  • Familiar with basic TypeScript types
  • Familiar with React.js
  • Reading the previous article
cover image

Naming Conventions:

  • use PascalCase for class, component and Enum names.
  • use camelCase for variable and function names.
  • do not use I as prefix of the interface instead use ComponentNameProps or FunctionNameParams.
  • use .generated.* as suffix for the files are generated and you don't edit them.
interface ComponentNameProps {
  position: 'left' | 'right'
}
function ComponentName({ position }: ComponentNameProps) {
  if (position === 'left') {
    return <div>left</div>
  }
  return <div>right</div>
}
interface FunctionNameParams {
  args: string[]
}
function functionName({ args }: FunctionNameParams) {
  return args.join(' ')
}

Types:

  • put the shared types in types.ts file.
  • use generics to avoid duplicating the components/functions.
  • do not export functions/types that not shared across multi-components.
  • in your component file, the type definitions must comes the first.
  • use Utility Types to avoid duplicating of the type definitions.
  • use readonly to define the data coming from your API

Components:

  • don't use FunctionComponent<T> or FC<T> to define your component, because it's explicit about the return type, while the normal function version is implicit, read more about this codemod-replace-react-fc-typescript.
  • use ReactNode instead JSX.Element to define your children when use the normal functions, because JSX.Element is the return value of React.createElement, while ReactNode is return value of a component
  • use generic components to make your component more flexible, read more about this topic.
  • use the React.ComponentPropsWithoutRef<T> as Wrapping/Mirroring a HTML Element when you want to make component that takes all the normal props of this Element.read more about this topic
// bad
interface MyCustomLinkProps {
  href: string
  children?: React.ReactNode
}
function MyCustomLink(props: MyCustomLinkProps) {
  const { children, href } = props
  return <a href={href}>{children}</a>
}
// most recommended
function MyCustomLink(props: React.ComponentPropsWithoutRef<'a'>) {
  return <a {...props}>{children}</a>
}
  • use Fragment or use the shorthand <> instead div in wrapping the component.
// bad
function ComponentName() {
  return (
    <div>
      <section></section>
      <section></section>
    </div>
  )
}
// most recommended
function ComponentName() {
  return (
    <>
      <section></section>
      <section></section>
    </>
  )
}

Forms and Events:

  • inline event handler:
const InputComponent = () => (
  <input
    // TypeScript automatically inffers it
    onChange={(e) => {
      // logic here
    }}
  />
)
  • separated event handler:
const InputComponent = () => {
  const onChnageInputHandler = (e: React.FormEvent<HTMLInputElement>): void => {
    // your logic here
  }
  return <input onChange={onChnageInputHandler} />
}

or:

const InputComponent = () => {
  const onChnageInputHandler: React.ChangeEventHandler<HTMLInputElement> = (
    e
  ) => {
    // your logic here
  }
  return <input onChange={onChnageInputHandler} />
}
  • onSubmit definition for uncontrolled form component
const Form = () => (
  <form
    onSubmit={(e: React.SyntheticEvent) => {
      e.preventDefault()
      const target = e.target as typeof e.target & {
        email: { value: string }
        password: { value: string }
      }
      const email = target.email.value // typechecks!
      const password = target.password.value // typechecks!
      // etc...
    }}
  >
    <label>
      Email:
      <input type="email" name="email" />
    </label>
    <label>
      Password:
      <input type="password" name="password" />
    </label>
    <button type="submit">Login</button>
  </form>
)

you can use formik or any other library for handling Forms, those libraries are built on the principles of controlled components

Hooks:

  • useState hook :
const Component = () => {
  //1- inferred type:
  // `state` is inferred to be a number
  const [state, setState] = useState(0)
  // `stringState` is inferred to be a string
  const [stringState, setStringState] = useState('')
  //2- implicit type:
  const [state1, setState2] = useState<number | undefined>()
  return <></>
}
  • useCallback hook :
//   it's just like any other function
interface MemoizedFunctionParams {
params:string[]
}
const memoizedFunction = useCallback(
  (args:MemoizedFunctionParams) => {
    return 'text'
  },
  [...],
);
  • useReducer hook:
import { useReducer } from 'react'

const initialState = { count: 0 }

type ACTIONTYPE =
  | { type: 'increment'; payload: number }
  | { type: 'decrement'; payload: string }

function reducer(state: typeof initialState, action: ACTIONTYPE) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + action.payload }
    case 'decrement':
      return { count: state.count - Number(action.payload) }
    default:
      throw new Error()
  }
}

function Counter() {
  // If you don't define return type, TypeScript automatically infers it.
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>
        -
      </button>
      <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>
        +
      </button>
    </>
  )
}
  • useRef hook :
import { useRef, useEffect } from 'react'
// This is just an example, React recommended you to use Controlled components
function Component() {
  const inputRef = useRef<HTMLInputElement>(null)
  const divRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!inputRef) {
      throw Error('input not mounted !!!!')
    }
    inputRef.current.value = 'assign value'
  }, [])
  const onChnageButtonHandler = () => {
    divRef.current.innerHTML = 'we change this text here'
  }
  return (
    <div>
      <input ref={inputRef} />
      <div ref={divRef}>this is div content</div>
      <button onClick={onChnageButtonHandler}>change the content</button>
    </div>
  )
}

Context:

  • with default value:
import { createContext, useState, useContext } from 'react'

type ThemeContextType = 'light' | 'dark'

const ThemeContext = createContext<ThemeContextType>('light')

const App = () => {
  const [theme, setTheme] = useState<ThemeContextType>('light')
  return (
    <ThemeContext.Provider value={theme}>
      <MyComponent />
    </ThemeContext.Provider>
  )
}
const MyComponent = () => {
  const theme = useContext(ThemeContext)
  return <p>The current theme is {theme}.</p>
}
  • without default value:
import { createContext, useState, useContext } from 'react'

type userContextType = { name: string; email: string }

const UserContext = createContext<userContextType>()

const App = () => {
  const [user, setUser] = useState<userContextType>({
    name: 'name',
    email: 'email@mail.com',
  })
  return (
    <ThemeContext.Provider value={...{ user, setUser }}>
      <MyComponent />
    </ThemeContext.Provider>
  )
}
const MyComponent = () => {
  const { user } = useContext(UserContext)
  return <p>The current theme is {user?.name}.</p>
}

Conclusion:

In conclusion, using TypeScript with React can write safer, more maintainable code, and catch errors earlier in the development process. Additionally, TypeScript's static typing can help improve developer productivity by providing better tooling and documentation.