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:
- Create a
TypeScript
app withcreate-react-app
- The
type
in TypeScript - Narrowing and discriminated unions
- Making our react app type-safe
- Using types with components, state, and reducer
- 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 ofnumber
. 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: