DEV Community

Cover image for Minimal TypeScript Crash Course For React - With Interactive Code Exercises
Johannes Kettmann
Johannes Kettmann

Posted on • Originally published at profy.dev

Minimal TypeScript Crash Course For React - With Interactive Code Exercises

Even if you haven’t worked with TypeScript yet, you probably heard about it. It’s seen vast adoption in the React world in the past years. As of today, almost all React jobs seem to require TypeScript knowledge.

So, many React devs ask themselves: Do I really need to learn TypeScript?

I get it, you have lots on your plate already. And especially if you’re still trying to break into the industry you’re likely overwhelmed with all the stuff to learn. But take it from this dev:

A developer’s opinion of TypeScript with React

Learning TypeScript is easily the best investment:

  • You’re chances to get a job increase.
  • Your code will be less buggy and easier to read/maintain.
  • Refactoring code and updating dependencies will be much easier.

In short, your developer experience will skyrocket. Still, you’ll likely have a rough time at first. Lots of strange errors and coding that feels like you have one hand tied behind your back (plus 4 of the remaining fingers glued together).

This page aims at getting you up to speed quickly without overwhelming you with all the details. You’ll get a rather minimal introduction to using TypeScript with React. To make the knowledge stick you can find interactive code exercises after almost every section.

Since this easily becomes a rather dry topic we loosely follow the anti-hero of this story: Pat, a nasty CTO.

Note that the code editor used for the exercises on this page is quite new. If you run into problems I’d appreciate a bug report at bugs@profy.dev.

TypeScript Basics Required For React

Primitives

Their name is diminishing, but the three primitives are at the core of all types.

string // e.g. "Pat"
boolean // e.g. true
number // e.g. 23 or 1.99
Enter fullscreen mode Exit fullscreen mode

Arrays

Arrays can be built from primitives or any other type.

number[] // e.g. [1, 2, 3]
string[] // e.g. ["Lisa", "Pat"]
User[] // custom type e.g. [{ name: "Pat" }, { name: "Lisa" }]
Enter fullscreen mode Exit fullscreen mode

Objects

Objects are everywhere and they can be really powerful. Just as in this example:

const user = {
  firstName: "Pat",
  age: 23,
  isNice: false,
  role: "CTO",
    skills: ["CSS", "HTML", "jQuery"]
}
Enter fullscreen mode Exit fullscreen mode

What a “CTO”! The TypeScript type describing this object looks like this:

type User = {
  firstName: string;
  age: number;
  isNice: boolean;
  role: string,
  skills: string[];
}
Enter fullscreen mode Exit fullscreen mode

Obviously, people don’t only consist of primitives.

type User = {
  firstName: string;
  ...
    friends: User[];
}
Enter fullscreen mode Exit fullscreen mode

But looking at our “CTO”, it’s good that we can make fields optional. Pat clearly chose his career over his friends:

const user = {
  firstName: "Pat",
  age: 23,
  isNice: false,
  role: "CTO",
    skills: ["CSS", "HTML", "jQuery"],
    friends: undefined
}
Enter fullscreen mode Exit fullscreen mode

As hard as this may be in the life of Pat, in TypeScript it’s as simple as adding a ?:

type User = {
  firstName: string;
  ...
    friends?: User[];
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

Enums

Remember, we defined the User.role field as a string.

type User = {
  ...
  role: string,
}
Enter fullscreen mode Exit fullscreen mode

Pat as the “CTO” isn’t happy about that. He knows that the string type isn’t restrictive enough. His employees shouldn’t be able to select any role they want.

Enums to his rescue!

enum UserRole {
  CEO,
  CTO,
  SUBORDINATE,
}
Enter fullscreen mode Exit fullscreen mode

This is much better! But Pat isn’t stupid: he knows that the values of this enum internally are only numbers. Even though the CEO is at the top (kiss-ass Pat) its numeric value is 0. Zero! The CTO is 1. And the subordinate is a 2?

Seems inappropriate. How come everyone's more valuable than the leadership?

Luckily, we can use string values for our enums instead.

enum UserRole {
  CEO = "ceo",
  CTO = "cto",
  SUBORDINATE = "inferior-person",
}
Enter fullscreen mode Exit fullscreen mode

This is very helpful when you want to get a message across (looking at you Pat). But it can also be useful when working with strings coming from an API.

Anyway, Pat is pleased now. He can safely assign anyone their appropriate role.

enum UserRole {
  CEO = "ceo",
  CTO = "cto",
  SUBORDINATE = "inferior-person",
}

type User = {
  firstName: string;
  age: number;
  isNice: boolean;
  role: UserRole,
  skills: string[];
  friends?: User[];
}

const user = {
  firstName: "Pat",
  age: 23,
  isNice: false,
  role: UserRole.CTO, // equals "cto"
    skills: ["CSS", "HTML", "jQuery"]
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

Function

What’s likely the favorite activity of any person in power? Not sure, but Pat certainly loves to demonstrate his power by firing a bunch of losers.

So let’s make Pat happy and write a function that increases his firing performance.

Typing Function Parameters

We have three ways to identify the person to be fired. First, we can use multiple parameters.

function fireUser(firstName: string, age: number, isNice: boolean) {
  ...
}

// alternatively as an arrow function
const fireUser = (firstName: string, age: number, isNice: boolean) => {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Second, by we can wrap all of the above parameters in an object and define the types inline.

function fireUser({ firstName, age, isNice }: {
  firstName: string;
  age: number;
  isNice: boolean;
}) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

And finally (since the above isn’t very readable) we can also extract the type. Spoiler alert: this is what we see with React component props a lot.

type User = {
  firstName: string;
  age: number;
  role: UserRole;
}

function fireUser({ firstName, age, role }: User) {
  ...
}

// alternatively as arrow function
const fireUser = ({ firstName, age, role }: User) => {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Typing Function Return Values

Simply firing a user might not be enough for Pat. Maybe he wants to insult them even more? So it might be a good idea to return the user from the function.

Again there are multiple ways of defining the return type. First we can add : MyType behind the closing bracket of the parameter list.

function fireUser(firstName: string, age: number, role: UserRole): User {
  // some logic to fire that loser ...
  return { firstName, age, role };
}

// alternatively as an arrow function
const fireUser = (firstName: string, age: number, role: UserRole): User => {
  ...
}
Enter fullscreen mode Exit fullscreen mode

This enforces that the correct type is returned. If we’d try to return something else (e.g. null) TypeScript wouldn’t let us.

TypeScript enforces correct return type

If we don’t want to be that strict, we can also let TypeScript infer the return type.

function fireUser(user: User) {
  // some logic to fire that loser ...
  return user;
}
Enter fullscreen mode Exit fullscreen mode

Here we simply return the input parameters. This could also be the return value of the “firing” logic. But since the type of the returned value user is clear TypeScript automatically knows the return type of the function.

TypeScript infers return value

So with TypeScript less is often better. You don’t need to define types everywhere but can often rely on type inference. When in doubt simply hover with your mouse cursor on top of the variable or function in question as shown in the screenshot above.

Live Coding Exercise

Things That You’ll Likely Encounter

Here are a few more things that we don’t cover here but you’ll encounter rather sooner than later.

React on the job - email course

React With TypeScript

When it comes to React and TypeScript the basics from above sections are usually sufficient. After all, React function components and hooks are simple functions. And props are just objects. In most cases, you don’t even need to define any types as TypeScript knows how to infer them.

Functional Components With TypeScript

For the most part, you don’t need to define the return type of your components. You’d definitely type the props (as we’ll see in a bit). But a simple prop-less component doesn’t need any types.

function UserProfile() {
  return <div>If you're Pat: YOU'RE AWESOME!!</div>
}
Enter fullscreen mode Exit fullscreen mode

The return type is JSX.Element as you can see in this screenshot.

React components return JSX.Element type

The great thing: If we mess up and return anything from a component that is not valid JSX, TypeScript warns us.

TypeScript warns when a component returns invalid JSX

In this case, a user object isn’t valid JSX so we get an error:

'UserProfile' cannot be used as a JSX component.
Its return type 'User' is not a valid JSX element.
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

Props With TypeScript

The UserProfile pays our “CTO” Pat a nice compliment. But it shows the same message for every user and that feels like an insult. Obviously, we need props.

enum UserRole {
  CEO = "ceo",
  CTO = "cto",
  SUBORDINATE = "inferior-person",
}

type UserProfileProps = {
  firstName: string;
  role: UserRole;
}

function UserProfile({ firstName, role }: UserProfileProps) {
    if (role === UserRole.CTO) {
    return <div>Hey Pat, you're AWESOME!!</div>
  }
  return <div>Hi {firstName}, you suck!</div>
}
Enter fullscreen mode Exit fullscreen mode

If you’re more into arrow functions the same component looks like this.

const UserProfile = ({ firstName, role }: UserProfileProps) => {
    if (role === UserRole.CTO) {
    return <div>Hey Pat, you're AWESOME!!</div>
  }
  return <div>Hi {firstName}, you suck!</div>
}
Enter fullscreen mode Exit fullscreen mode

A final note: In the wild, you find a lot of code that uses React.FC or React.FunctionComponent to type components. This is not recommended anymore though.

// using React.FC is not recommended
const UserProfile: React.FC<UserProfileProps>({ firstName, role }) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

Callback Props

As you know, in React we often pass around callback functions as props. So far we haven’t seen fields of type function. So let me quickly show you:

type UserProfileProps = {
  id: string;
  firstName: string;
  role: UserRole;
  fireUser: (id: string) => void;
};

function UserProfile({ id, firstName, role, fireUser }: UserProfileProps) {
  if (role === UserRole.CTO) {
    return <div>Hey Pat, you're AWESOME!!</div>;
  }
  return (
    <>
      <div>Hi {firstName}, you suck!</div>
      <button onClick={() => fireUser(id)}>Fire this loser!</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note: void is the return type of the function and stands for “nothing”.

Live Coding Exercise

Default Props

Remember: we can make a field optional by marking it with ?. We can do the same with optional props. Here the role isn’t required.

type UserProfileProps = {
  age: number;
  role?: UserRole;
}
Enter fullscreen mode Exit fullscreen mode

If we want to have a default value for an optional prop we can assign it when restructuring the props.

function UserProfile({ firstName, role = UserRole.SUBORDINATE }: UserProfileProps) {
  if (role === UserRole.CTO) {
    return <div>Hey Pat, you're AWESOME!!</div>
  }
  return <div>Hi {firstName}, you suck!</div>
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

useState Hook With TypeScript (Inferred Type)

The most used hook in React is useState(). And in many cases, you don’t need to type it. If you use an initial value TypeScript can infer the type.

function UserProfile({ firstName, role }: UserProfileProps) {
  const [isFired, setIsFired] = useState(false);
  return (
    <>
      <div>Hi {firstName}, you suck!</div>
      <button onClick={() => setIsFired(!isFired)}>
        {isFired ? "Oops, hire them back!" : "Fire this loser!"}
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

TypeScript infers type of React state

Now we’re safe. If we try to update the state with anything else than a boolean we get an error.

TypeScript warns when setting the wrong type as state

React on the job - email course

useState Hook With TypeScript (Manually Typed)

In other cases, TypeScript can’t infer the type properly from the initial value though. For example:

// TypeScript doesn't know what type the array elements should have
const [names, setNames] = useState([]);

// The initial value is undefined so TS doesn't know its actual type
const [user, setUser] = useState();

// Same story when we use null as initial value
const user = useState(null);
Enter fullscreen mode Exit fullscreen mode

In some situations TypeScript can’t infer the type of the state

useState() is implemented with a so-called generic type. We can use this to type our state properly:

// the type of names is string[]
const [names, setNames] = useState<string[]>([]);
setNames(["Pat", "Lisa"]);

// the type of user is User | undefined (we can either set a user or undefined)
const [user, setUser] = useState<User>();
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(undefined);

// the type of user is User | null (we can either set a user or null)
const [user, setUser] = useState<User | null>(null);
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(null);
Enter fullscreen mode Exit fullscreen mode

This should give you enough to work with useState(). As we focus on the minimal TypeScript skills for React we won’t discuss the other hooks here. Just note that useEffect() doesn’t need typing.

Live Coding Exercise

Custom Hooks With TypeScript

A custom hook is again just a function. So you already know how to type it.

function useFireUser(firstName: string) {
    const [isFired, setIsFired] = useState(false);
  const hireAndFire = () => setIsFired(!isFired);

    return {
    text: isFired ? `Oops, hire ${firstName} back!` : "Fire this loser!",
    hireAndFire,
  };
}

function UserProfile({ firstName, role }: UserProfileProps) {
  const { text, hireAndFire } = useFireUser(firstName);
  return (
    <>
      <div>Hi {firstName}, you suck!</div>
      <button onClick={hireAndFire}>
        {text}
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

React Events With TypeScript

Working with inline click handlers is simple as TypeScript knows the type of the event parameter already. You don’t need to type anything manually.

function FireButton() {
  return (
    <button onClick={(event) => event.preventDefault()}>
      Fire this loser!
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

It becomes more complicated when you create separate click handlers though.

function FireButton() {
  const onClick = (event: ???) => {
    event.preventDefault();
  };

  return (
    <button onClick={onClick}>
      Fire this loser!
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

What’s the type of event here? Here are two approaches:

  1. Search on Google (can’t recommend, makes you dizzy).
  2. Pretend you have an inline function and let TypeScript serve you the type on a silver platter.

Hover over the inline function parameter to get the correct event type

Happy copy & pasting. You don’t even need to understand what’s going on here (hint: these are generics as we saw them with useState).

function FireButton() {
  const onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    event.preventDefault();
  };

  return (
    <button onClick={onClick}>
      Fire this loser!
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

What about a change handler on an input? The same strategy reveals:

function Input() {
  const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    console.log(event.target.value);
  };

  return <input onChange={onChange} />;
}
Enter fullscreen mode Exit fullscreen mode

And a select?

function Select() {
  const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    console.log(event.target.value);
  };

  return <select onChange={onChange}>...</select>;
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

Children Type Or Component Type

As we’re all fans of component composition we need to know how to type the common children prop.

type LayoutProps = {
  children: React.ReactNode;
};

function Layout({ children }: LayoutProps) {
  return <div>{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The type React.ReactNode gives you a lot of freedom. It basically lets us pass anything as a child (except an object).

React.ReactNode type allows everything but objects

If we want to be a bit stricter and only allow markup we can use React.ReactElement or JSX.Element (which is basically the same).

type LayoutProps = {
  children: React.ReactElement; // same as JSX.Element
};
Enter fullscreen mode Exit fullscreen mode

As you can see, this is much more restrictive:

React.ReactElement type allows only JSX

React on the job - email course

Working With Third-Party Libraries

Adding Types

Nowadays, many third-party libraries already ship with their corresponding types. In that case, you don’t need to install a separate package.

But many types are also maintained in the DefinitelyTyped repository on GitHub and published under the @types organization (even the React types). If you install a library without types you get an error message on the import statement.

Many third-party libraries required additional types in the @types registry

You can simply copy & paste the highlighted command and execute it in the terminal.

npm i --save-dev @types/styled-components
Enter fullscreen mode Exit fullscreen mode

Most of the time, you’ll be lucky and the required types are available in some way or the other. Especially for more popular libraries. But if you’re not you can still define your own global types in a .d.ts file (we won’t cover this here though).

Using Generics

Libraries often need to handle a lot of use cases. And thus they need to be flexible. To define flexible types generics are used. We’ve seen them with useState already. Here’s a reminder:

const [names, setNames] = useState<string[]>([]);
Enter fullscreen mode Exit fullscreen mode

This is very common in many third-party libraries. Here is Axios as an example:

import axios from "axios"

async function fetchUser() {
  const response = await axios.get<User>("https://example.com/api/user");
  return response.data;
}
Enter fullscreen mode Exit fullscreen mode

third-party libraries often use generic types

Or react-query:

import { useQuery } from "@tanstack/react-query";

function UserProfile() {
  // generic types for data and error
  const { data, error } = useQuery<User, Error>(["user"], () => fetchUser());

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  ...
}
Enter fullscreen mode Exit fullscreen mode

Or styled-components:

import styled from "styled-components";

// generic type for props
const MenuItem = styled.li<{ isActive: boolean }>`
  background: ${(props) => (props.isActive ? "red" : "gray")};
`;

function Menu() {
  return (
    <ul>
      <MenuItem isActive>Menu Item 1</MenuItem>
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

Strategies & Troubleshooting

Getting Started With React & TypeScript

Creating a new project with TypeScript is the easiest option. I recommend either creating a Vite + React + TypeScript or Next.js + TypeScript project.

// for Vite run this command and select "react-ts"
npm create vite@latest

// for Next.js run
npx create-next-app@latest --ts
Enter fullscreen mode Exit fullscreen mode

This will set you up completely automatically.

Finding The Right Type

We’ve touched on that already but let me repeat it here: When in doubt (especially useful with events) just start typing an inline function and let TypeScript show you the correct type.

Hover over the inline function parameter to get the correct event type

You can do the same if you’re not sure how many parameters are available. Just write (...args) => and you get all parameters in an array.

Use spread operator to get all function parameters

Inspecting A Type

The easiest way of seeing all available fields on a type is by using the autocomplete feature of your IDE. Here by pressing CTRL + space (Windows) or Option + space (Mac).

Inspecting a TypeScript type with autocomplete suggestions

Sometimes you need to dig deeper though. This is simple (yet often confusing) by CTRL + click (Windows) or CMD + click (Mac) to go to the type definitions.

Inspecting a TypeScript type by following the definitions

React on the job - email course

Reading Error Messages

One major problem when getting started with TypeScript is all the errors you encounter. All the things that can go wrong can drive you nuts. So it’s a good idea to get used to read the error messages. Let’s take this as an example:

function Input() {
  return <input />;
}

function Form() {
  return (
    <form>
      <Input onChange={() => console.log("change")} />
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

You might see the problem already. Still, here is the error that TypeScript shows.

TypeScript error message: prop is not defined

Time to get confused! What the heck does this mean? What is the type IntrinsicAttributes? When working with libraries (e.g. React itself) you’ll encounter many strange type names like this.

My advice: ignore them for now.

The most important piece of information is in the last line:

Property 'onChange' does not exist on type ...
Enter fullscreen mode Exit fullscreen mode

Does that ring a bell? Look at the definition of the <Input> component:

function Input() {
  return <input />;
}
Enter fullscreen mode Exit fullscreen mode

It doesn’t have a prop called onChange. That’s what TypeScript is complaining about.

Now, this was a rather simple example. But what about this?

const MenuItem = styled.li`
  background: "red";
`;

function Menu() {
  return <MenuItem isActive>Menu Item</MenuItem>;
}
Enter fullscreen mode Exit fullscreen mode

TypeScript error message: no overload matches this call

Holy cow! It’s easy to be confused by the sheer amount of error output here. My favorite technique here is to scroll to the bottom of the message. More often than not the golden nugget is buried in there.

TypeScript error message: no overload matches this call solution is at the bottom

function UserProfile() {
  const { data, error } = useQuery(
    ["user"],
    {
      cacheTime: 100000,
    },
    () => fetchUser()
  );
}
Enter fullscreen mode Exit fullscreen mode

Objects As Props For Cleaner Types

In the above example, the user data would usually come from an API. Let’s assume we have the User type defined outside of the component

export type User = {
  firstName: string;
  role: UserRole;
}
Enter fullscreen mode Exit fullscreen mode

The UserProfile component at the moment takes exactly this object as props.

function UserProfile({ firstName, role }: User) { 
  ...
}
Enter fullscreen mode Exit fullscreen mode

This might seem reasonable for now. And it’s in fact pretty easy to render this component when we have a ready-made user object.

function UserPage() {
  const user = useFetchUser();
  return <UserProfile {...user} />;
}
Enter fullscreen mode Exit fullscreen mode

But as soon as we want to add additional props that are not included in the User type we make our lives harder. Remember the fireUserLOL function from above? Let’s put it to use.

function UserProfile({ firstName, role, fireUser }: User) {
  return (
    <>
      <div>Hi {firstName}, you suck!</div>
      <button onClick={() => fireUser({ firstName, role })}>
        Fire this loser!
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

But since the fireUser function isn’t defined on the User type we get an error.

Mixing different props types

To create the correct type we can use a so-called intersection type by combining two types with &. This basically takes all fields from two different types and merges them into a single type.

type User = {
  firstName: string;
  role: UserRole;
}

// this is called an intersection
// UserProfileProps has all fields of both types
type UserProfileProps = User & {
    fireUser: (user: User) => void;
}

function UserProfile({ firstName, role, fireUser }: UserProfileProps) {
  return (
    <>
      <div>Hi {firstName}, you suck!</div>
      <button onClick={() => fireUser({ firstName, role })}>
        Fire this loser!
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Instead of intersection types, it’s often cleaner to separate the types. In our case, we can use a user prop instead of directly accepting all the user fields.

type User = {
  firstName: string;
  role: UserRole;
}

type UserProfileProps = {
  user: User;
    fireUser: (user: User) => void;
}

function UserProfile({ user, onClick }: UserProfileProps) {
  return (
    <>
      <div>Hi {user.firstName}, you suck!</div>
      <button onClick={() => fireUser(user)}>
        Fire this loser!
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Live Coding Exercise

React on the job - email course

Top comments (3)

Collapse
 
sujitmohanty profile image
Sujit Mohanty

This is beyond amazing! Thank you very much!

Collapse
 
jkettmann profile image
Johannes Kettmann

Thanks a lot for the feedback :)

Collapse
 
yukikimoto profile image
Yuki Kimoto

Thanks.