If you're learning React with TypeScript, you've probably encountered code like this:

type User = { name: string; age: number; }; function UserCard({ user }: { user: User }) { return <h2>{user.name}</h2>; } 

But what exactly is type? Why do we use User? What does : User mean? What's the difference between type and interface? And how do TypeScript, React props, state, events, and hooks work together?

This guide covers the TypeScript fundamentals you actually need to build React applications confidently.

We'll start with the basics and gradually move into real-world React patterns.

Table of Contents

  1. What Is TypeScript?
  2. JavaScript vs. TypeScript
  3. Type Annotations
  4. Basic Types
  5. Type Inference
  6. Arrays and Objects
  7. type vs. interface
  8. Union and Literal Types
  9. Optional Properties
  10. Functions and Return Types
  11. Type Aliases and Reusable Types
  12. Type Narrowing
  13. Generics
  14. TypeScript with React Components
  15. Typing Props
  16. Optional Props and Default Values
  17. Children and ReactNode
  18. Event Handling
  19. useState
  20. useEffect
  21. useRef
  22. useReducer
  23. Context API
  24. Custom Hooks
  25. Forms
  26. API Requests
  27. Utility Types
  28. Enums vs. Literal Unions
  29. any, unknown, and never
  30. Type Assertions
  31. Common TypeScript Mistakes
  32. Recommended Learning Path

1. What Is TypeScript?

TypeScript is a programming language developed by Microsoft.

It is a superset of JavaScript, meaning that valid JavaScript code is generally valid TypeScript code.

TypeScript adds a static type system to JavaScript.

Consider this JavaScript function:

function add(a, b) { return a + b; } add(10, 20); add("Hello", "World"); 

JavaScript allows both calls.

But what if the function is supposed to work only with numbers?

TypeScript allows us to express that intention:

function add(a: number, b: number): number { return a + b; } add(10, 20); // TypeScript error add("Hello", "World"); 

Now TypeScript can detect an incorrect function call before the application runs.

Does TypeScript run in the browser?

No. Browsers execute JavaScript, not TypeScript.

TypeScript code must be transformed into JavaScript before execution.

TypeScript ↓ Type checking and compilation ↓ JavaScript ↓ Browser 

Tools such as Vite, Next.js, and other build systems handle this transformation.

Important: TypeScript types are primarily checked during development and are erased from the generated JavaScript. They do not automatically validate data coming from an API at runtime.

2. JavaScript vs. TypeScript

JavaScript:

function getUserName(user) { return user.name; } 

TypeScript:

function getUserName(user: { name: string }): string { return user.name; } 

The TypeScript version tells us:

  • user must have a name property.
  • name must be a string.
  • The function returns a string.

TypeScript helps developers catch mistakes earlier, improve autocomplete, and understand code more easily.

3. Type Annotations

A type annotation tells TypeScript what type a variable, parameter, or return value should have.

The syntax is:

variableName: Type 

Examples:

let username: string = "Hamed"; let age: number = 26; let isActive: boolean = true; 

Now consider:

let age: number = 26; age = 30; // TypeScript error age = "Thirty"; 

The variable is declared as a number, so assigning a string causes a type error.

Function parameters

function greet(name: string) { return `Hello, ${name}`; } greet("React Developer"); 

Function return types

function multiply(a: number, b: number): number { return a * b; } 

The : number after the parameter list specifies the function's return type.

4. Basic TypeScript Types

String

let firstName: string = "John"; 

Number

let score: number = 100; 

TypeScript uses number for integers and floating-point numbers.

Boolean

let isLoggedIn: boolean = false; 

Null and Undefined

let emptyValue: null = null; let missingValue: undefined = undefined; 

These types are especially important when working with optional values.

Object

let user: { name: string; age: number; } = { name: "John", age: 25, }; 

Any

let value: any = "Hello"; value = 100; value = true; 

any disables type checking for that value.

It can be convenient temporarily, but using it everywhere removes many of TypeScript's benefits.

We'll discuss safer alternatives later.

5. Type Inference

TypeScript can often determine a variable's type automatically.

This is called type inference.

let username = "John"; 

TypeScript understands that username is a string.

Therefore, writing this is usually unnecessary:

let username: string = "John"; 

Both examples work, but the first is more concise.

Another example:

let count = 10; count = 20; // TypeScript error count = "Twenty"; 

TypeScript inferred count as a number.

When should you explicitly define a type?

Use explicit types when they improve clarity or when TypeScript cannot infer the intended type.

Common examples include:

  • Function parameters
  • Component props
  • API response models
  • Complex objects
  • State that can contain multiple types
  • Reusable functions and hooks

In React, you'll often see type annotations around component props and state.

6. Arrays and Objects

Arrays

An array of strings:

const names: string[] = ["John", "Sarah", "Alex"]; 

An array of numbers:

const scores: number[] = [10, 20, 30]; 

Another syntax uses Array<T>:

const names: Array<string> = ["John", "Sarah"]; 

Both approaches are valid.

For most React applications, string[] and number[] are easier to read.

Arrays of objects

Imagine an application displaying a list of users.

type User = { id: number; name: string; email: string; }; const users: User[] = [ { id: 1, name: "John", email: "john@example.com", }, { id: 2, name: "Sarah", email: "sarah@example.com", }, ]; 

Now every user must follow the User structure.

const newUser: User = { id: 3, name: "Alex", email: "alex@example.com", }; 

This becomes extremely useful when rendering lists in React.

{users.map((user) => ( <div key={user.id}> <h2>{user.name}</h2> <p>{user.email}</p> </div> ))} 

7. type vs. interface

Both type and interface allow you to describe the structure of an object.

Using type

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

Using interface

interface User { id: number; name: string; email: string; } 

For this example, both behave similarly.

Extending an interface

interface User { id: number; name: string; } interface Admin extends User { permissions: string[]; } 

Now Admin includes the properties from User and adds permissions.

const admin: Admin = { id: 1, name: "John", permissions: ["read", "write"], }; 

Extending a type

type User = { id: number; name: string; }; type Admin = User & { permissions: string[]; }; 

The & operator combines types. This is called an intersection type.

What's the difference?

  • interface is commonly used to describe object shapes.
  • type can describe objects, unions, primitives, tuples, and more.
  • Interfaces can be reopened and merged through declaration merging.
  • Type aliases cannot be reopened in the same way.

For React projects, either is perfectly acceptable.

A common convention is:

interface ButtonProps { label: string; } type Status = "loading" | "success" | "error"; 

The most important thing is consistency within your project.

8. Union Types and Literal Types

A union type allows a value to have more than one possible type.

let value: string | number; value = "Hello"; value = 100; 

The | symbol means OR.

Union types in React

Imagine a component that receives either a username or a numeric user ID.

type UserIdentifier = string | number; function getUser(id: UserIdentifier) { console.log(id); } 

Both calls are valid:

getUser("john"); getUser(123); 

Literal types

Literal types restrict a value to specific options.

type Status = "loading" | "success" | "error"; 

Now:

let status: Status = "loading"; status = "success"; // TypeScript error status = "completed"; 

Literal unions are extremely useful in React.

type ButtonVariant = "primary" | "secondary" | "danger"; 
interface ButtonProps { variant: ButtonVariant; children: React.ReactNode; } function Button({ variant, children }: ButtonProps) { return ( <button className={`button button-${variant}`}> {children} </button> ); } 

Now the component only accepts the supported variants.

<Button variant="primary">Save</Button> <Button variant="danger">Delete</Button> // TypeScript error <Button variant="banana">Click</Button> 

9. Optional Properties

Sometimes an object does not need every property.

Use ? to make a property optional.

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

Both objects are valid:

const user1: User = { name: "John", }; const user2: User = { name: "Sarah", age: 25, }; 

The age property may be present or absent.

However, accessing it requires care:

console.log(user1.age); 

The result may be undefined.

Optional properties in React

interface GreetingProps { name: string; subtitle?: string; } function Greeting({ name, subtitle }: GreetingProps) { return ( <div> <h1>Hello, {name}</h1> {subtitle && <p>{subtitle}</p>} </div> ); } 

Usage:

<Greeting name="John" /> <Greeting name="Sarah" subtitle="Welcome back!" /> 

10. Functions and Return Types

TypeScript allows us to define the types of function parameters and return values.

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

The function accepts two numbers and returns a number.

Arrow functions

const subtract = (a: number, b: number): number => { return a - b; }; 

Functions that return nothing

function logMessage(message: string): void { console.log(message); } 

void indicates that the function's return value is not intended to be used.

Function types

type Calculator = (a: number, b: number) => number; const multiply: Calculator = (a, b) => { return a * b; }; 

This is useful for callbacks and React props.

interface ButtonProps { onClick: () => void; } 

The onClick property is a function that accepts no arguments and returns nothing meaningful.

11. Reusable Types

Instead of repeatedly writing the same object structure, create reusable types.

Without a reusable type:

function getUserName(user: { id: number; name: string; }) { return user.name; } function getUserId(user: { id: number; name: string; }) { return user.id; } 

This creates unnecessary duplication.

With a reusable type:

type User = { id: number; name: string; }; function getUserName(user: User): string { return user.name; } function getUserId(user: User): number { return user.id; } 

This becomes especially valuable in large React applications.

You might define types for:

  • User
  • Product
  • Post
  • API response
  • Form values
  • Component props

A common project structure might look like:

src/ ├── components/ │ ├── Button.tsx │ └── UserCard.tsx ├── types/ │ └── user.ts └── App.tsx 

Example user.ts:

export interface User { id: number; name: string; email: string; } 

Then import it:

import type { User } from "../types/user"; 

The import type syntax explicitly imports a type rather than a runtime value.

12. Type Narrowing

Type narrowing means making a broad type more specific after checking its value.

Consider:

function printValue(value: string | number) { console.log(value); } 

The value could be a string or a number.

But what if we want to call a string-specific method?

function printValue(value: string | number) { if (typeof value === "string") { console.log(value.toUpperCase()); } else { console.log(value.toFixed(2)); } } 

TypeScript understands:

  • Inside the first branch, value is a string.
  • Inside the second branch, value is a number.

This is called type narrowing.

Narrowing with typeof

function formatValue(value: string | number) { if (typeof value === "number") { return value.toFixed(2); } return value.toUpperCase(); } 

Narrowing with in

type Admin = { role: "admin"; permissions: string[]; }; type User = { role: "user"; name: stringSource:https://dev.to/hamedfarazi/typescript-for-react-developers-the-complete-practical-guide-57am

Related posts