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
- What Is TypeScript?
- JavaScript vs. TypeScript
- Type Annotations
- Basic Types
- Type Inference
- Arrays and Objects
-
typevs.interface - Union and Literal Types
- Optional Properties
- Functions and Return Types
- Type Aliases and Reusable Types
- Type Narrowing
- Generics
- TypeScript with React Components
- Typing Props
- Optional Props and Default Values
- Children and ReactNode
- Event Handling
- useState
- useEffect
- useRef
- useReducer
- Context API
- Custom Hooks
- Forms
- API Requests
- Utility Types
- Enums vs. Literal Unions
-
any,unknown, andnever - Type Assertions
- Common TypeScript Mistakes
- 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:
-
usermust have anameproperty. -
namemust 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?
-
interfaceis commonly used to describe object shapes. -
typecan 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,
valueis a string. - Inside the second branch,
valueis 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