Published on

Converting to TypeScript from React JS

2620 words14 min read
Authors
  • avatar
    Name
    Curtis Warcup
    Twitter

Typing Functions

After the function name, you must add a colon and the type of the function. In this case, we are typing the function that returns a boolean. We need to state what the function takes in and what it returns.

const func: (a: string, b: number, c: boolean) => boolean = (a, b, c) => {
  return true
  // must return a boolean
}

We can also have functions that do not return anything. In this case, we would use the void keyword.

const func: (a: string, b: number, c: boolean) => void = (a, b, c) => {
  console.log('hello')
  // does not return anything
}

Typing In React

Typing Props

We have two ways of typing props: interface and type.

interfacetype
Are extendable. Similar to classesAre not extendable

Use this component as an example:

const SearchBox = ({ className, placeholder, onChangeHandler }) => (
  <input
    className={`search-box ${className}`}
    type="search"
    placeholder={placeholder}
    onChange={onChangeHandler}
  />
)

export default SearchBox

To props is an object that contains the props that are passed into the component. We can type the props by adding a colon and the type of the props. In this case, we are typing the props as an object that contains a className, placeholder, and onChangeHandler property.

We will use the interface keyword to type the props.

interface SearchBoxProps {
  // describe the shape of the props object
}

Here we would say the props is an object that contains a className, placeholder, and onChangeHandler property.

We can also tell typescript that a property is optional by using the ? symbol.

interface SearchBoxProps {
  className: string;
  placeholder?: string;
  onChangeHandler: // but what type is this?
}

How do we type the onChangeHandler property? We can use the type keyword to type the onChangeHandler property.

Extending Interfaces

Remember how we said that interfaces are extendable? We can use the extends keyword to extend the SearchBoxProps interface.

interface SearchBoxProps extends SearchBoxProps {
  className: string;
  placeholder?: string;
  onChangeHandler: // but what type is this?
}

interface someChangeHandlerProps {
  // describe the shape of the onChangeHandler props
  onChangeHandler: (a: string) => void;
}

Overloading Interfaces

interface SearchBoxProps {
  className: string
  placeholder: string
}

interface SearchBoxProps {
  // describe the shape of the onChangeHandler props
  onChangeHandler: (event: React.ChangeEvent<HTMLInputElement>) => void
}

const SearchBox = ({ className, placeholder, onChangeHandler }: SearchBoxProps) => (
  <input
    className={`search-box ${className}`}
    type="search"
    placeholder={placeholder}
    onChange={onChangeHandler}
  />
)

Use the same name for the interface to overload it.

Types

  • do not allow for extension
  • can not overload them

So why do we like to use types? UNIONS!

We can use the | symbol to create a union type. This means that the type can be one of the types that we specify.

For example, what if we needed to create a type of an address? US and Canada have different address formats. We can use a union type to specify that the type can be a US address or a Canadian address.

type Address = {
  province: string
  state: string
}

How do we handle this?

type CanadianAddress = {
  street: string
  province: string
}

type USAddress = {
  street: string
  state: string
}

// make a union type
type NorthAmericanAddress = CanadianAddress | USAddress

This means that our NorthAmericanAddress type can be a CanadianAddress or a USAddress.

This example would work fine:

const address: NorthAmericanAddress = {
  street: '123 Main St',
  province: 'Ontario',
}

Typing Props

Back to the SearchBox component:

type SearchBoxProps = {
  className: string
  placeholder?: string | undefined
  onChangeHandler: ChangeEventHandler<HTMLInputElement>
}

const SearchBox = ({ className, placeholder, onChangeHandler }: SearchBoxProps) => (
  <input
    className={`search-box ${className}`}
    type="search"
    placeholder={placeholder}
    onChange={onChangeHandler}
  />
)

The onChangeHandler property is a function that takes in an event and returns nothing. We can use the ChangeEventHandler type to type the onChangeHandler property.

The onChange function is being provided by the input element. We can use the ChangeEventHandler type to type the onChange function. This is why we need to pass the ChangeEventHandler the HTMLInputElement type.


Another example with a different component:

type Monster = {
  id: number
  name: string
  email: string
}

type CardProps = {
  monster: Monster
}

const Card = ({ monster }: CardProps) => {
  const { id, name, email } = monster

  return (
    <div className="card-container">
      <img alt={`monster ${name}`} src={`https://robohash.org/${id}?set=set2&size=180x180`} />
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  )
}

export default Card

Generics

Typing Fetched Data

When we make a fetch request, we don't know exactly what we will get. Sometimes there's no data, sometimes there's an error, and sometimes there's data.

fetch('https://jsonplaceholder.typicode.com/users')
  .then((response) => response.json())
  .then((users) => setMonsters(users))

We can create a utility function to handle the different cases.

// fetchData.ts

export const getData = async (url) => {
  const response = await fetch(url)
  return await response.json()
}

We know that the URL will be a string.

But what does this function return? It could be a number of things.

export const getData = async (url: string):RETURN_TYPE_HERE??? => {
  const response = await fetch(url);
  return await response.json();
}

Since this is an async function, we can use the Promise type to type the function.

export const getData = async (url: string):Promise<????> => {
  const response = await fetch(url);
  return await response.json();
}

What should we put in the Promise type? TypeScript does not know.

But we know the data we are getting back. This is where we can use a generic type.

export const getData = async <T>(url: string): Promise<T> => {
  const response = await fetch(url)
  return await response.json()
}

Here we are saying that the function will return a Promise that resolves to a T. We need to specify what T is.

Using Generics

import { getData } from './fetchData'

// we know we have some async function
const fetchUsers = async () => {
  const users = await getData<OUR_T_TYPE_HERE>('https://jsonplaceholder.typicode.com/users')
  ///.... continued
}

We haven't told typescript what T is. We need to specify what T is. We know that the data we are getting back is an array of objects.

type Monster = {
  id: number
  name: string
  email: string
}

const [monsters, setMonsters] = useState([])

const fetchUsers = async () => {
  const users = await getData<Monster[]>('https://jsonplaceholder.typicode.com/users')
  setMonsters(users)
}

Types for useState

Now we get a new error: Argument of type 'Monster[]' is not assignable to parameter of type 'SetStateAction<never[]>'.

This is because in our useState hook, we didn't specify what type of data we are getting back. We use the useState hook in the setMonsters function.

// original
const [monsters, setMonsters] = useState([]) // we need to specify what type of data we are getting back

// updated
const [monsters, setMonsters] = useState<Monster[]>([])

In the updated version of the code, we are saying that the useState hook will return an array of Monster objects.

We only need to explicitly state the type of data we are getting back when we have not initialized the state.

Take this for example:

const [searchField, setSearchField] = useState('')

Typescript infers that the type of data we are getting back is a string because we initialized the state with an empty string.

Same goes for something like this:

type Monster = {
  id: number
  name: string
  email: string
}

const [filteredMonsters, setFilteredMonsters] = useState(monsters)

We initialized filteredMonsters with the monsters array. TypeScript infers that the type of data we are getting back is an array of Monster objects.

However, if we do this, we have not initialized the state:

const [searchField, setSearchField] = useState([])

Types for onChangeHandler

Take this jsx for example:

// some component

const onSearchChange = (event) => {
  const searchFieldString = event.target.value.toLocaleLowerCase()
  setSearchField(searchFieldString)
}

return (
  <>
    <h1 className="app-title">Monsters Rolodex</h1>
    <SearchBox
      className="monsters-search-box"
      onChangeHandler={onSearchChange} //
      placeholder="search monsters"
    />
    <CardList monsters={filteredMonsters} />
  </>
)

Here we have an onChangeHandler function that takes in an event, gets the value of the input, and sets the searchField state. When we convert this to typescript, we get an error: Parameter 'event' implicitly has an 'any' type.

We know we are going to return void from this function.

We also know that this is change event which receives an HTML input.

Recall our SearchBox component:

const SearchBox = ({ className, onChangeHandler, placeholder }: SearchBoxProps) => (
  <input
    className={`search-box ${className}`}
    type="search"
    placeholder={placeholder}
    onChange={onChangeHandler}
  />
)

We can update our onChangeHandler function to take in an HTMLInputElement:

  const onSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
    const searchFieldString = event.target.value.toLocaleLowerCase();
    setSearchField(searchFieldString);
  };

  return (
    <>
      <h1 className="app-title">Monsters Rolodex</h1>
        <SearchBox
          className="monsters-search-box"
          onChangeHandler={onSearchChange}
          placeholder="search monsters"
        />
      <CardList monsters={filteredMonsters} />
    <>
  );