TypeScript: Make your React Apps even more powerful!

TypeScript: Make your React Apps even more powerful!

ยท

8 min read

Wanted to .map() over a value but that didn't turn out to be an array?
Passed a prop to a component but forgot its type?
Used .split() on it but it was not a string?

My JavaScript brain can relate to this. But, my TypeScript brain can not.
It's hard for me to do these mistakes now. How?
Because of TypeScript of course!

Why?

See, JavaScript gives you a lot of freedom, freedom of dynamic typing of variables. But, with freedom comes responsibility and the moment you get a little lazy or things start bouncing off, JS is out of control before you know it.

TypeScript forces you to state the type of a variable beforehand to save you from these run-time errors in the future. Yes, it does feel like a lot of work initially but my friend, it pays off.
It pays off really well.

Lets start

Before starting, let's see how this blog flows:

  1. Create a TypeScript app with create-react-app
  2. The type in TypeScript
  3. Narrowing and discriminated unions
  4. Making our react app type-safe
  5. Using types with components, state, and reducer
  6. Do's and Don'ts of TypeScript

It's gonna be a long ride. So, grab your coffee or buy me one โ˜•

Create a TypeScript app with create-react-app

In your terminal, write:

npx create-react-app my-first-ts-app --template typescript

This will create a TS app named my-first-ts-app from the typescript template provided by create-react-app. cd into this folder and run npm start.
There we go, our first TS app is up and running.
You will notice that all .js file extension has been changed to .ts and .jsx to .tsx. That's TypeScript's extension.

The type in TypeScript

We define the type of a variable as:

const variableName: type = value;

Lets take a look at three primitive data types:

  • boolean

             const isPresent: boolean = true;
    
  • number

             const count: number = 11;
    
  • string

             const userName: string = 'Kushank';
    

Also, we have some more types based on the primitives:

  • array
    Array of strings:

             const users: string[] = ['Kushank', 'Sandeep', 'Megha'];
    

    Array of numbers:

             const scores: number[] = [12, 11, 21];
    
  • any
    Use this when you don't want to do type-checking on a variable.

             const someValue: any = '๐Ÿคทโ€โ™‚๏ธ';
    

Most of the time, we use objects, right? Let's see how to write type for an object.

The type:

    type MyObject = {
        name: string;
        score: number;
    }

The object:

    const myObject: MyObject = {
        name: 'Kushank',
        score: 99
    }

Narrowing and discriminated unions

Let's take an example to better understand the union of types:
Suppose, you have a variable named toggle to show or hide the password. So, its type can be a boolean as follows:

const toggle: boolean = true;

But, what if you want toggle to store a string that will show exactly what this toggle is for? This is better in some cases. So, you will have a string type as follows:

const toggle: string = "SHOW";

What if you can keep both? This might seem impossible at first but TypeScript gives us a way to do that also. Let's see:

const toggle: boolean | string = true;

Yes, that's one pipe sign of the typical OR statement (||). This is how we can have multiple types for one variable. Now, we can assign toggle a string or a boolean, both will work.

Now, a problem could arise in the future when you are using a variable whose type is a union of multiple types.
I hope you can guess it. No?

Let's see.
Let the type be a union of number and string.

let assignMe: number | string = 5;

Now if we try to do this ๐Ÿ‘‡

console.log(assignMe - 3);

We get an error:

`The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type`

So, basically, TypeScript is giving an error because it is unable to identify the exact type of assignMe. It could be a number or a string.
We need to narrow it down!

Here comes the type guard: a way to narrow down to one type out of all the types of the union of types.
We can do it with a simple if-else statement.

if(typeof assignMe === "number") {
    console.log(assignMe - 3); 
    // -> 2
 }
else {
  console.log(assignMe, " is a string!");
  // -> 5 is a string!
}

Phew! No errors!
TypeScript intelligently identifies the type based on the type guard and throws no error as the code is now error-free.

Now, let's see how we can do this with multiple variables or an object.
This is called Discriminated union.

 type CarType =
  |  { name: "Tesla"; chargingHours: number } 
  |  { name: "Lamborghini"; fuelType: string }

This way, an object can have both the types using CarType:

const carOne: CarType = {
    name: "Tesla",
    chargingHours: 10
}

Here, if we try to put fuelType as a property, it will give an error because the type of object with a property name as "Tesla" doesn't have fuelType as a property.

// Also,
const carTwo: CarType = {
    name: "Lamborghini",
    fuelType: "Petrol"
}

Now, if we want to do any operation on these objects, we will have to narrow down to one particular type out of the union of types using a type guard as shown below:

const getCarInfo = (carObj : CarType): number | string => {
    if(carObj.name === "Tesla") {
        // returns a number
        return carObj.chargingHours;
    }
    // returns a string
    return carObj.fuelType;
}

console.log(getCarInfo(carOne);
// -> 10

console.log(getCarInfo(carTwo);
// -> Petrol

Making our react app type-safe

When we created our react app using the npx command, we also got a tsconfig.json file at the root of our project.
Check it out. It is used to control how much and what level of type-checking you want in your app.
If you are a beginner, I suggest you not change anything on that file.
By default, the type-checking is enabled in our app and we are good to go.

Using types with components, state, and reducer

We are going to build a simple increment and decrement counter. We will have two components: Control and Header. We are going to use useReducer for state management.

First, lets define all the types required and export it.
counter.types.ts:

export type CounterType = {
  count: number;
  status: "Begin" | "Continue" | "Stop";
};

export type ActionType =
  | { type: "INCREMENT"; payload: { status: "Begin" | "Continue" | "Stop" } }
  | { type: "DECREMENT"; payload: { status: "Begin" | "Continue" | "Stop" } }
  | { type: "RESET" };

export type HeaderProp = {
  state: CounterType;
};

export type ControlProp = {
  state: CounterType;
  dispatch: React.Dispatch<ActionType>;
};

As you can see, I have used discriminated union as a type for the action object.

Now, lets write our two components: Header and Control.
Header.tsx:

import { HeaderProp } from "../counter.types";

export const Header = (props: HeaderProp) => {
  return (
    <header>
      <h2>
        counter value: <span> {props.state.count}</span>
      </h2>
      <h3>
        Status: <span> {props.state.status}</span>
      </h3>
    </header>
  );
};

Here, I am importing the HeaderProp type and using it in the component.

Control.tsx:

import { ControlProp } from "../counter.types";

const getStatus = (count: number): "Begin" | "Continue" | "Stop" => {
  if (count === 0) return "Begin";
  if (count > 0 && count < 10) return "Continue";
  return "Stop";
};

export const Control = ({ state: { count }, dispatch }: ControlProp) => {
  const clickHandler = (type: "INCREMENT" | "DECREMENT" | "RESET") => {
    switch (type) {
      case "INCREMENT":
        dispatch({
          type: "INCREMENT",
          payload: {
            status: getStatus(count + 1),
          },
        });
        break;

      case "DECREMENT":
        dispatch({
          type: "DECREMENT",
          payload: {
            status: getStatus(count - 1),
          },
        });
        break;

      case "RESET":
        dispatch({
          type: "RESET",
        });
        break;

      default:
        break;
    }
  };

  return (
    <main>
      <h3>Counter controls:</h3>
      <div>
        <button onClick={() => clickHandler("DECREMENT")}>- Decrease</button>
        <button onClick={() => clickHandler("INCREMENT")}>+ Increase</button>
        <button onClick={() => clickHandler("RESET")}>Reset</button>
      </div>
    </main>
  );
};

I have two helper functions: getStatus and clickHandler which are also type-checked. Yes, you can enforce the return type of a function also.

Here is the App.tsx:

import "./Style.css";
import { useReducer } from "react";
import { counterReducer } from "./counterReducer";
import { Header, Control } from "./Components";
import { CounterType } from "./counter.types";

const initialState: CounterType = {
  count: 0,
  status: "Begin",
};

function App() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div className="App">
      <Header state={state} />
      <Control state={state} dispatch={dispatch} />
    </div>
  );
}

export default App;

Finally, putting everything together.

Oh, how can we miss the reducer! Here it is:

counterReducer.ts:

import { CounterType, ActionType } from "./counter.types";

export const counterReducer = (
  state: CounterType,
  action: ActionType
): CounterType => {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + 1, status: action.payload.status };

    case "DECREMENT":
      return { count: state.count - 1, status: action.payload.status };

    case "RESET":
      return { count: 0, status: "Begin" };

    default:
      return state;
  }
};

And with that, our react app is done. It's strongly type-checked and functional.
Check it out: Live link, codesandbox

Do's and Don'ts of TypeScript

  • When I was first starting with it, I accidentally put type as Number instead of number. Never do this mistake. As said in the TypeScript docs:

    These types refer to non-primitive boxed objects that are rarely used appropriately in JavaScript code.

  • Always export your types so that you can use them in multiple places, as I did in the react example above.

  • Alternatively, you can define the types first and then go on fixing the error, that's kind of TypeScript Driven Development

Check out this beginner-friendly TypeScript handbook.

This is just the surface of TypeScript, there's a lot more to it.
Keep exploring, experimenting, and learning. And yeah, typing...๐Ÿ‘จโ€๐Ÿ’ป

Thank you for reading this relatively long article. I hope you enjoyed it.
Please share and comment below your views on TypeScript.

Connect with me on LinkedIn.
And if you would like to support me, buy me a coffee!

References:

ย