✨ Typescript to Know for React

A no-fluff guide to switching from JavaScript to TypeScript in your React apps

Two weeks ago, I stumbled upon this post on Reddit: Should I learn React with TypeScript or JavaScript?

I see these posts all the time—and honestly, it breaks my heart 🥲

If you’re a React dev still using JavaScript, you’re making your life harder.

The time you spend learning TypeScript will 100% pay off in the time you save debugging weird bugs.

So here’s a guide to help you make the switch.

Just the essentials. No fluff 😉.

How TypeScript works

TypeScript adds extra syntax on top of JavaScript to help catch errors early—before they crash your app.

It compiles to JavaScript in the end.

The best part? You can slowly migrate to TypeScript.

Follow this guide for more.

Why you should switch to TypeScript ASAP

Here’s why I think it’s a no-brainer:

Benefit #1: Catch bugs before they reach production

This JavaScript code looks fine… until it explodes:

function printFirstChar(users, userIndex) {
  const name = users[userIndex];
  console.log(name[0]);
}

const users = ['Fatou', 'Bob'];

printFirstChar(users, 5); // ❌ Runtime error: Cannot read properties of undefined

Here’s the TypeScript version:

function printFirstChar(users: string[], userIndex: number) {
  const name = users[userIndex];

  if (name != null) {
    console.log(name[0]);
  }
}

TypeScript doesn’t fix your logic, but it gives you early warnings like “hey, this might be undefined 👀

Benefit #2: Get autocompletion with libraries

Especially useful when you're exploring a new library.

If that library has TypeScript types, you can get autocompletion inside most editors.

Benefit #3: Make your code more resilient to changes

When you type things properly, TypeScript acts like a safety net.

For example:

Without TypeScript, this code can break without you noticing.
function getLabel(status) {
  switch (status) {
    case 'idle':
      return 'Idle';
    case 'loading':
      return 'Loading';
    case 'success':
      return 'Success';
  }
}

getLabel('loading'); // ✅ works

// Months later...

getLabel('error'); // ❌ returns undefined, but no JS error
With TypeScript, you need to handle changes.
type Status = 'idle' | 'loading' | 'success';

function getLabel(status: Status): string {
  switch (status) {
    case 'idle':
      return 'Idle';
    case 'loading':
      return 'Loading';
    case 'success':
      return 'Success';
    case 'error':
      return 'Error';
  }
}

// Months later...
type Status = 'idle' | 'loading' | 'success' | 'error'; // added 'error'

// ✅ TypeScript will show an error for the `getLabel` function: "Not all code paths return a value"

If you forget to handle a case, TypeScript yells at you. And that’s precisely what you want 😄.

TypeScript error when we forget to handle a case

The TypeScript concepts you need for React

Let’s cover the bare minimum to get started.

Types

Types let you describe the shape of your data and enforce it.

There are native types in TypeScript, and types defined by users.

Native types

These types come by default.

const name = 'Ada'; // string
const age: number = 31; // number
const isActive = true; // boolean
const scores: number[] = [90, 85, 100]; // number[]

💡 In some of the examples above, I typed values explicitly (like const age: number = 31).

You don’t need to do that in real code—TypeScript can infer the types by looking at the values.

Custom types

You can create new types using the native types.

type User = {
  id: number;
  name: string;
};

const user: User = {
  id: 1,
  name: 'Fatou',
};

Avoid any

Unless you’re in the middle of a migration, don’t use any.

It’s JavaScript in disguise.

Use unknown instead—it forces you to check the type before using it.

// ❌ Bad: no warning, crashes at runtime
function logLength(value: any) {
  console.log(value.length);
}

logLength(123);

// ✅ Safer
function logLength(value: unknown) {
  if (typeof value === 'string' || Array.isArray(value)) {
    console.log(value.length);
  }
}

// ✅✅ Better: use an explicit type
function logLength(value: string | ArrayLike<unknown>){
  console.log(value.length);
}

🧩 Union & intersection types

Unions let a value be one of many types.

type Status = 'idle' | 'loading' | 'error';

let currentStatus: Status = 'idle';

Intersections combine multiple types into one.

type Name = { name: string };
type Age = { age: number };

type Person = Name & Age;

const person: Person = { name: 'Ada', age: 30 };

🧱 Interfaces

Interfaces define the structure of an object.

interface User {
  name: string;
  age: number;
}

You can extend interfaces:

interface Admin extends User {
  role: string;
}

When to use type vs interface? Doesn’t matter much.

But here’s an interesting guide:

🔁 Generics

Generics let you write reusable types with flexible data.

Let’s say you’re typing an API response.

Without generics:

type UserResponse = {
  status: number;
  data: User;
  error?: string;
};

type ProductResponse = {
  status: number;
  data: Product;
  error?: string;
};

That’s a lot of repetition.

With generics:

type ApiResponse<T> = {
  status: number;
  data: T;
  error?: string;
};

type UserResponse = ApiResponse<User>;
type ProductResponse = ApiResponse<Product>;

You can use generics in functions too:

function wrap<T>(value: T): T[] {
  return [value];
}

wrap(42); // type: number[]
wrap('hi'); // type: string[]

I like to think of generics like function parameters—but for types 😉.

🔧 Function types

You can type function parameters and return values.

You always need to type the parameters, but don’t need to type the returned value every time.

// This works
function add(a: number, b: number): number {
  return a + b;
}

// This also works: you don't need to type the returned value
function add(a: number, b: number) {
  return a + b; // inferred as number
}

You can also type the function variable:

const add: (a: number, b: number) => number = (a, b) => a + b;

Depending on the context, you may want to type the return type. When in doubt, type the return type.

You can learn more about return types here:

How to use TypeScript with React

Props

Props are just objects. Type them like any other object.

With type:

type GreetingProps = {
  name: string;
};

function Greeting({ name }: GreetingProps) {
  return <p>Hello {name}</p>;
}

With interface :

interface GreetingProps {
  name: string;
}

function Greeting({ name }: GreetingProps) {
  return <p>Hello {name}</p>;
}

State

useState accepts a generic type, but TypeScript can also infer it.

const [count, setCount] = useState(0); // inferred as number

But if it can’t, be explicit:

const [user, setUser] = useState<{ id: number; name: string } | null>(null);

Component variables and functions

Just like normal TypeScript:

const users: User[] = [{name: "Fatou", age: 31}, {name: "Bob", age: 25} ];
const age = 30; // inferred as number

When you have some functions like event handlers, you can hover over the DOM elements to see the types to use.

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  console.log('Clicked!', event);
};

// Or:
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (event) => {
  console.log('Clicked!', event);
};

Re-using types across components

You can extend or combine types/interfaces to avoid duplication.

With interfaces:

interface ButtonProps {
  label: string;
}

interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode;
}

With union types:

type ButtonProps = {
  label: string;
};

type IconButtonProps = ButtonProps & {
  icon: React.ReactNode;
};

Hooks

Hooks are just functions, so you can use the same TypeScript tools that apply to functions.

Native hooks

Pretty much all the native hooks have generic params.

const [count, setCount] = useState<number>(0);
// or let TS infer:
const [count, setCount] = useState(0);

const inputRef = useRef<HTMLInputElement>(null);

const [state, dispatch] = useReducer(
  (state: number, action: 'inc' | 'dec') => {
    switch (action) {
      case 'inc':
        return state + 1;
      case 'dec':
        return state - 1;
    }
  },
  0
);

Custom hooks

function useToggle(initial: boolean): [boolean, () => void] {
  const [state, setState] = useState(initial);
  const toggle = () => setState((prev) => !prev);
  return [state, toggle];
}

const [isOpen, toggleOpen] = useToggle(false);

Where to learn more

If you want to go deeper with TypeScript, here are some great resources to explore:

  • 🧠 TypeScript Official Docs: The best place to start. It’s clear, well-structured, and packed with examples. Also includes a playground to test things out.

  • 🎯 Total TypeScript: A fantastic site by Matt Pocock—especially for React devs.
    It has a free beginner course, advanced patterns, and many real-world examples.

  •  TypeScript Style Guide: Short, practical tips on how to write clean and consistent TypeScript code. Highly recommended once you start building real apps.

  • 🧩 Type Challenges: A collection of TypeScript puzzles—from beginner to insane. Great if you learn best by solving problems and want to flex your type system skills.

  • 📘 Effective TypeScript: A book + blog by Dan Vanderkam. More advanced, but teaches you how to write better TypeScript. Worth it once you know the basics.

  • ⚛️ React TypeScript Cheatsheets: Tailored for React devs. Covers everything from typing components to context and hooks. Very beginner-friendly, and super helpful as a quick reference.

💡 Make sure to also check out the TypeScript section of my 101 React Tips & Tricks post 😉.

Summary

Using TypeScript with React is one of the best upgrades you can make.

It helps you catch bugs early, gives you better tooling, and makes your code easier to maintain.

The best part? You don’t need to learn everything up front. Start with the basics—types, interfaces, generics—and grow from there.

Many examples are online if you get stuck, and AI tools can help you unblock fast.

Let me know if you have any questions 🙂.

Reply

or to participate.