DEV Community

loading...
Cover image for React + Typescript === "headache"

React + Typescript === "headache"

Vinicius Cerqueira Bonifácio
JavaScript developer, technology passionate, Ubuntu evangelist, ex mountain guide, active football player, inactive surfer, professional backpacker and hitchhiker.
Updated on ・6 min read

Hi, dear devs.

Long time I haven't written anything here. I hope everybody is doing well.

Before you think I am here making criticism about the Typescript language, you are right! Naah, I am just kidding. My intention is just to describe my experience with the React + Typescript alliance. (it sounds like Star Wars :))

I won't explain in depth what Typescript is because there are tons of articles out there doing it, but for this article is enough knowing that it adds static typing to Javascript. static typing x dynamic typing

Since long ago I have been playing around with TS but actually I have never used it in any of my projects for a reason: TS suppose to be used in big applications, people said.

But why should you also try it?

Like I mentioned, it is widely used in big applications so if you know Typescript you are already one step ahead of other candidates when applying for jobs. 😅

AWS Experience

This time I have adventured myself into building a very small application using React + TS and below I related how it was.

PS.: I expect you have at least the basics of React, hooks, CLI, axios and VSCode because I will focus in explaining just the TS features.

The adventure starts here. Enjoy the ride! 🎒

Installation takes several steps:

npm install --global typescript
Enter fullscreen mode Exit fullscreen mode
npx create-react-app ts-react-app --template typescript
Enter fullscreen mode Exit fullscreen mode
npm install --save axios
Enter fullscreen mode Exit fullscreen mode
cd ts-react-app && code .
Enter fullscreen mode Exit fullscreen mode

Our React application has a tsconfig.json file and the components have the .tsx extension.

If you see any difference between your application and mine it is because I have cleaned up and deleted a bunch of unnecessary files for the purpose of this article.

This is the so familiar React App file. Nothing new at the moment.

import React from "react";
import PokemonContainer from "./components/PokemonContainer";

function App() {
  return (
    <div>
      <PokemonContainer />
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Inside of the components folder, we have the PokemonContainer.tsx file and its content:

import React, { useState, useEffect } from "react";
import Pokemon from "../interfaces/Pokemon.interface";
import axios from "axios";

const PokemonContainer: React.FC = () => {
  const [pokemon, setPokemon]: [Pokemon, (pokemon: Pokemon) => void] = useState<Pokemon | null>(null);
  const [loading, setLoading]: [boolean, (loading: boolean) => void] = useState<
    boolean
  >(false);
  const [error, setError]: [string, (error: string) => void] = useState(
    "",
  );
  const [inputName, setInputName]: [string, (inputName: string) => void] = React
    .useState("bulbasaur");

  const pokemonRef: React.RefObject<HTMLInputElement> = React.createRef();

  const onSearchHandler = (): void => {
    setInputName(pokemonRef.current.value.toLowerCase());
  };

  useEffect(() => {
    setLoading(true);
    setError("");
    axios.get(`https://pokeapi.co/api/v2/pokemon/${inputName}`, {
      headers: {
        "Content-Type": "application/json",
      },
    })
      .then((response) => {
        setLoading(false);

        setPokemon(
          {
            name: response.data.name,
            base_experience: response.data.base_experience,
            imageURL: response.data.sprites.front_default,
            numberOfAbilities: response.data.abilities.length,
          },
        );
      })
      .catch((error) => {
        setLoading(false);
        setError(error.message);
      });
  }, [inputName]);

  return (
    <div>
      {loading && <div>Loading ...</div>}
      {error && <div>{error}</div>}
      {pokemon &&
        <div>
          <img src={pokemon.imageURL} alt="pokemon-pic" />
          <h3>{pokemon.name}</h3>
          <p>Base EXP: {pokemon.base_experience}</p>
          <p>Abilities: {pokemon.numberOfAbilities}</p>
        </div>}

      <div>Please, type the pokemon name below</div>
      <input type="text" ref={pokemonRef} />

      <button
        onClick={() => onSearchHandler()}
      >
        Search
      </button>
    </div>
  );
};

export default PokemonContainer;

Enter fullscreen mode Exit fullscreen mode

Super Overwhelming, right?!

Indeed but don't be intimidated for that. I am going to give my best to make you understand it. I promise it will worth your time.

Import Section

You don't really need me to explain it, do you? :)

// It imports stuff haha
import React, { useState, useEffect } from "react";
import Pokemon from "../interfaces/Pokemon.interface";
import axios from "axios";
Enter fullscreen mode Exit fullscreen mode

Creating Our Functional Component

const PokemonContainer: React.FC = () => { ... }
Enter fullscreen mode Exit fullscreen mode

The line : React.FC indicates that our component PokemonContainer has the React Functional type (FC).

In Typescript you could define the types like:

let myName :string = "Vinicius"
let myAge :number = 99
Enter fullscreen mode Exit fullscreen mode

but the correct way, in this case would be:

let myName = "Vinicius"
let myAge = 99
Enter fullscreen mode Exit fullscreen mode

because Typescript has the called type inference on it.

(Thanks people in the comments from pointing it out. 😃)

Even tough, you are not only limited to those types you know. You can create your own types and interfaces. types x interfaces

Interface

Coming from the ./src/interface/Pokemon.interface.ts file:

interface Pokemon {
  name: string;
  base_experience: number;
  numberOfAbilities: number;
  imageURL: string;
}

export default Pokemon;
Enter fullscreen mode Exit fullscreen mode

We will fetch data from the Pokemon API using axios. The Pokemon interface we created explicitly defines the types of its fields. It avoids mistakes, for example, if for some reason someone tries to assign a number as the value for the pokemon name.

Typing the useState hooks values

const [pokemon, setPokemon]: [Pokemon, (pokemon: Pokemon) => void] = useState<Pokemon | null>(null);
  const [loading, setLoading]: [boolean, (loading: boolean) => void] = useState<boolean>(false);
  const [error, setError]: [string, (error: string) => void] = useState("");
  const [inputName, setInputName]: [string, (inputName: string) => void] = React.useState("bulbasaur");
Enter fullscreen mode Exit fullscreen mode

It looks more complicated than it really is. It is known that the useState hook returns the state and a function to change this state.
As an example I will explain the const [pokemon, setPokemon]: [Pokemon, (pokemon: Pokemon) => void] = useState(null); statement. The explanation though applies to all the useState cases in this file.

  • [pokemon, ...] is the state from the hook so its type is pokemon: Pokemon.
  • [..., setPokemon] is the function responsible to change the state so it is type is (pokemon: Pokemon) => void. The void type indicates that the function doesn't return any value, it only sets the new state of the pokemon.
  • useState(null); indicates that the state the pokemon hook will receive is either of Pokemon or null types. The | (pipe) is equivalent an OR (||) condition. Conditional Types

Create Ref

const pokemonRef: React.RefObject<HTMLInputElement> = React.createRef();
Enter fullscreen mode Exit fullscreen mode

I had no idea about how to typing a ref before (and believe you won't like to memorize all the existent types) but one cool feature is the Typescript Intellisense that suggests how you should type "stuff". It is not 100% accurate (yet) but it helps a lot.

Last line of explanation 🏆 🎈 🎉 🍾

setPokemon({
            name: response.data.name,
            base_experience: response.data.base_experience,
            imageURL: response.data.sprites.front_default,
            numberOfAbilities: response.data.abilities.length,
          },
        );
Enter fullscreen mode Exit fullscreen mode

It is not directly related to Typescript. I just want you to try something: assign the value of base_experience to name and vice-versa to see what happens. 😃

Correction [EDITED x 2]

Thanks a lot for you guys that interacted with my article.
Our colleague stereobooster created one amazing version of the same example that you can check below and also in the comments. 😊

import React, { useState, useEffect } from "react";
import axios from "axios";

type Pokemon = {
  name: string,
  base_experience: number,
  numberOfAbilities: number,
  imageURL: string
}

type UseGet<Data> = {
    data: Data | undefined,
    loading: boolean,
    error: string  | undefined
}

const useGetPokemon = (name: string) => {
  const [state, setState] = useState<UseGet<Pokemon>>({
    loading: false,
    error: undefined
    data: undefined,
  });

  useEffect(() => {
    const source = axios.CancelToken.source();
    const cancelToken = source.token;
    setState({
      loading: true,
      error: undefined,
      data: undefined,
    });
    axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`, {
        cancelToken,
        headers: { "Content-Type": "application/json" },
      }).then(({ data }) => {
        setState({
          loading: false,
          error: undefined,
          data: {
            name: data.name,
            base_experience: data.base_experience,
            imageURL: data.sprites.front_default,
            numberOfAbilities: data.abilities.length,
          },
        });
      }).catch((error) => {
        if (axios.isCancel(error)) {
          setState({
            loading: false,
            error: error.message,
            data: undefined,
          });
        }
    });
    return () => source.cancel();
  }, [name, setState]);

  return state;
}

const PokemonSearch: React.FC = () => {
  const [inputName, setInputName] = React.useState("bulbasaur");
  const { data: pokemon, error, loading } = useGetPokemon(inputName);
  // ...
}
Enter fullscreen mode Exit fullscreen mode

My Conclusion

Typescript is for sure a great language and it can avoid typing problems when developing applications already in the development phase. Now I see clearly why big companies have been adopting it in their projects. Spotting problems as soon as they come to existence can save lots of money and resources.

On the other hand, it makes the development process slower if compared with the conventional Javascript mode. It also confuses a lot the newcomers (like me) that already have a background developing with Javascript.

I will definitely not choose TS in my personal projects given their size but as I mentioned previously, it worth having this knowledge.

PS.: This article just reflects my personal opinion about using Typescript and barely scratches the surface of it.

If you really want to learn it, the TS Official Documentation is the right place for you to go. 🗒️

Also here, as suggested in the comments React TypeScript Cheatsheet. 🙌

Thanks for reach the end and Happy Hacking! 🙏

  • Source code only for one's sake. here. 💻

Discussion (21)

Collapse
stereobooster profile image
stereobooster

I like the title and the cover image.

TypeScript can do type inference, so you don't need to provide types everywhere.

This will work the same:

let myName = "Vinicius"
let myAge = 99
const [pokemon, setPokemon] = useState<Pokemon | null>(null);
Enter fullscreen mode Exit fullscreen mode

Also I would not use separate file for interface, just place it in the top of the component (it will make your life a bit easier)

Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Thanks a lot for your comment and insights. :)

I will definitively adopt your approach and also refactor the code following your suggestions.

Collapse
stereobooster profile image
stereobooster • Edited
import React, { useState, useEffect } from "react";
import axios from "axios";

type Pokemon = {
  name: string,
  base_experience: number,
  numberOfAbilities: number,
  imageURL: string
}

type UseGet<Data> = {
    data: Data | undefined,
    loading: boolean,
    error: string  | undefined
}

const useGetPokemon = (name: string) => {
  const [state, setState] = useState<UseGet<Pokemon>>({
    loading: false,
    error: undefined
    data: undefined,
  });

  useEffect(() => {
    const source = axios.CancelToken.source();
    const cancelToken = source.token;
    setState({
      loading: true,
      error: undefined,
      data: undefined,
    });
    axios.get(`https://pokeapi.co/api/v2/pokemon/${name}`, {
        cancelToken,
        headers: { "Content-Type": "application/json" },
      }).then(({ data }) => {
        setState({
          loading: false,
          error: undefined,
          data: {
            name: data.name,
            base_experience: data.base_experience,
            imageURL: data.sprites.front_default,
            numberOfAbilities: data.abilities.length,
          },
        });
      }).catch((error) => {
        if (!axios.isCancel(error)) {
          setState({
            loading: false,
            error: error.message,
            data: undefined,
          });
        }
    });
    return () => source.cancel();
  }, [name, setState]);

  return state;
}

const PokemonSearch: React.FC = () => {
  const [inputName, setInputName] = React.useState("bulbasaur");
  const { data: pokemon, error, loading } = useGetPokemon(inputName);
  // ...
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author • Edited

What could I say? You definitively nailed it! 👏

I have to admit that reading your code brought me some kind of satisfaction.

Let's agree (or not) that being my first time trying TS I was not that bad, I guess. 😅
Also in my defense I didn't invest too much time in other parts of the code but sir, let's be honest here, your version rocks!

By the way, may I ask you one question?
I have noticed the way you created the cancellation token and I would like to know it the approach below is also right because I have been using it since always. I mean, regarding good practices.

useEffect(() => {
    let cancel; 
    axios
        .get(`https://pokeapi.co/api/v2/pokemon/${name}`, {
        cancelToken: new axios.CancelToken((ctoken) => (cancel = ctoken)),
      })
    //...
    return () => cancel();
}
Enter fullscreen mode Exit fullscreen mode

Thanks again for participating, I really appreciate it! 🙏

Thread Thread
stereobooster profile image
stereobooster • Edited

I never meant it as a challenge. I know that using TS is hard and wanted to show alternative approach.

Regarding cancel question: theoretically provided code can go wrong because of asynchronous functions, but probably in practice you will never see problems. In your code TS will complain about cancel being used before assignment (probably)

Thread Thread
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Please, don't get me wrong. It was not a challenge at all.

I really just wanted to report my first experience with TS but I think it is important to highlight the alternative you provided, for people like me who are giving the first steps with the language. For example, I'll surely use it as a reference and probably other people too.

Thanks for the explanation about the cancelling token. Until now I have had no problems but considering changing the way I use it. :)

I have learned a lot from the example you provided! Thanks for that.

Collapse
ecyrbe profile image
ecyrbe

You should not add typings to things you get back from a function, (like useState), there is what is called type inference and you should use it extensively.
You usually only need to add typing to your input parameters if you use type inference correctly.

even in c++ now everybody uses type inference everywhere.

Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Thanks for the hint!

I am going to be aware of type inference from now on. :)

Collapse
peacefullatom profile image
Yuriy Markov

Great article, thank you!
By the way, I'm using typescript everywhere and it makes my life easier. Especially if you return to the codebase which you haven't touched for a long time :)

Collapse
andrewbaisden profile image
Andrew Baisden

Agreed TypeScript is much better once you get the hang of it.

Collapse
peacefullatom profile image
Yuriy Markov

Just keep on learning. With time it will become your friend :)

Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Hi, Yuriy.

Thanks a lot for your comment. I really appreciate it. :)

I tried to describe my first-time experience using TS instead JS. I exaggerated typing more than I should as other developer spotted it but I really enjoyed the ride!

Did you face the same issues when started using TS? Feel free to share your experience with us. :)

I am sure it is very handy when dealing again with the codebase after a while. Trying to figure out what code does is always very time consuming. At least for me it is. XD

Collapse
peacefullatom profile image
Yuriy Markov

Well, for me this story went a bit different. :)
My first programming language was C++. So, when I switched to JS it was a pretty nightmare for me - the lack of types. :D
And when the TS emerged I was very happy. From my point of view, it gets better with each release.
Happy coding!

Thread Thread
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Really interesting story, Yuri. Thanks for sharing it!

I have never tried C++ but I read about it and it seems a very powerful tool.

I kind of understand the nightmare you had. Java was my first language ever but I ended up having my first job experience as a Ruby on Rails developer. 😂 It was kind of funny.

In my humble opinion, Java and Typescript look like similar somehow. Please don't judge if you don't. :D

I am just giving the first steps with TS but after a while getting lost and messing up a lot I started to like it. Hehe

Happy Hacking!

Collapse
akashkava profile image
Akash Kava

Typing has limits in functional programming, TypeScript offers good solution for 90% of the time, but it’s impossible to provide 100% typing and that requires lot of efforts as well. But as with other typed languages, TypeScript comes very handy with OOPS, and having best of both makes it easier.

Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Thanks for commenting, Akash. :)

I still have lots to learn about both TS and typing but I totally agree with you. 100% typing is hard, takes much time and effort indeed and probably it is not something ideal in most of the cases.

Collapse
redbar0n profile image
Magne • Edited

Try extracting the data fetching into a custom hook, for reusability, like shown in here in Approach A: dev.to/suhanw/decouple-data-from-u...

It is essentially what React Query does for you, which I would consider to look into vs Axios. That might reduce the boilerplate code considerably.

PS: The example would have been nice with a non-typescript version of the code, side-by-side. So we can see what headache came because of TypeScript vs. what came because of Axios.

Collapse
vishnumohanrk profile image
Vishnumohan R K
Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Thanks for commenting, Vishnumohan.

I will surely check it out and also add this link into this article as another reference.

Collapse
thesanjeevsharma profile image
Sanjeev Sharma

I started using typescript recently. It frustrates me so much some times but I still love it. Paired with intellisense, it is a life saver.

Collapse
vinicius77 profile image
Vinicius Cerqueira Bonifácio Author

Hi, Sanjeev.

Thanks for commenting. :)

I am also started using it recently and I totally understand your frustration. It is the same as mine. jeje To be honest, my first experience with TS was really good even I have "typed" more than I should as you can notice.

Intellisense is a very nice tool and indeed is a life saver.