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: string;
};
function displayAccount(account: Admin | User) {
if ("permissions" in account) {
console.log(account.permissions);
} else {
console.log(account.name);
}
}
Type narrowing is important when working with API responses and complex React state.
13. Generics
Generics are one of the most powerful TypeScript concepts.
They allow you to create reusable functions, types, and components while preserving type information.
Imagine this function:
function getValue(value: number): number {
return value;
}
It only works with numbers.
We could create another function for strings:
function getString(value: string): string {
return value;
}
But this duplicates logic.
Generics solve this problem.
Basic generic function
function identity<T>(value: T): T {
return value;
}
The T represents a type that will be determined when the function is used.
const numberValue = identity(100);
const stringValue = identity("Hello");
TypeScript understands:
numberValue // number
stringValue // string
T is a convention, not a special keyword. You could use another name, but T is commonly used for "Type".
Generic arrays
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = getFirstItem([10, 20, 30]);
const firstName = getFirstItem(["John", "Sarah"]);
TypeScript infers the type from the array.
Generics in React
Imagine a reusable list component.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>
{renderItem(item)}
</li>
))}
</ul>
);
}
Usage:
const names = ["John", "Sarah", "Alex"];
<List
items={names}
renderItem={(name) => <span>{name}</span>}
/>
TypeScript infers that name is a string.
For a more complex example:
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "John" },
{ id: 2, name: "Sarah" },
];
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
/>
Now user is automatically typed as User.
You don't need to manually annotate it.
14. TypeScript with React Components
React components are functions that return React elements.
A simple component:
function Welcome() {
return <h1>Hello React!</h1>;
}
TypeScript can infer the return type.
In most cases, you don't need to explicitly write:
function Welcome(): JSX.Element {
return <h1>Hello React!</h1>;
}
Modern React projects typically allow TypeScript to infer the component return type.
The most important thing to type explicitly is usually the component's props.
15. Typing React Props
Props are values passed from a parent component to a child component.
JavaScript:
function UserCard(props) {
return <h2>{props.name}</h2>;
}
TypeScript:
interface UserCardProps {
name: string;
age: number;
}
function UserCard({ name, age }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>{age}</p>
</div>
);
}
Usage:
<UserCard name="John" age={25} />
Incorrect usage:
<UserCard name="John" age="Twenty-five" />
TypeScript detects that age must be a number.
Why destructuring works
This syntax:
function UserCard({ name, age }: UserCardProps) {
// ...
}
Means:
- The component receives an object.
- The object must follow
UserCardProps. - We extract
nameandagefrom that object.
You can also write:
function UserCard(props: UserCardProps) {
return <h2>{props.name}</h2>;
}
Both approaches are valid.
16. Optional Props and Default Values
A component might have required and optional props.
interface ButtonProps {
label: string;
disabled?: boolean;
}
Usage:
<Button label="Save" />
<Button label="Save" disabled={true} />
You can provide a default value:
interface ButtonProps {
label: string;
disabled?: boolean;
}
function Button({ label, disabled = false }: ButtonProps) {
return (
<button disabled={disabled}>
{label}
</button>
);
}
Now disabled defaults to false.
This is a common pattern in React components.
17. Typing Children
React components often receive content between their opening and closing tags.
<Card>
<h2>Hello</h2>
</Card>
That content is called children.
The common type for arbitrary renderable React content is React.ReactNode.
import type { ReactNode } from "react";
interface CardProps {
children: ReactNode;
}
function Card({ children }: CardProps) {
return <div className="card">{children}</div>;
}
Usage:
<Card>
<h2>User Profile</h2>
<p>Welcome back!</p>
</Card>
ReactNode can represent many kinds of renderable content, including:
- React elements
- Strings
- Numbers
- Arrays of renderable nodes
nullundefined
Optional children
interface CardProps {
children?: ReactNode;
}
Use this when the component can be rendered without children.
ReactNode vs. ReactElement
ReactNode is broader and generally used when a component accepts arbitrary renderable content.
ReactElement describes a React element object.
For most component children props, ReactNode is the practical choice.
18. Typing React Events
React event handlers are one of the most important TypeScript topics.
Consider:
function Input() {
return <input onChange={(event) => console.log(event)} />;
}
React and TypeScript can often infer the event type automatically.
However, sometimes you need to define it explicitly.
Input events
import type { ChangeEvent } from "react";
function Input() {
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
return <input onChange={handleChange} />;
}
The type:
ChangeEvent<HTMLInputElement>
means this is a change event associated with an HTML input element.
Textarea events
import type { ChangeEvent } from "react";
function TextArea() {
const handleChange = (
event: ChangeEvent<HTMLTextAreaElement>
) => {
console.log(event.target.value);
};
return <textarea onChange={handleChange} />;
}
Select events
import type { ChangeEvent } from "react";
function Select() {
const handleChange = (
event: ChangeEvent<HTMLSelectElement>
) => {
console.log(event.target.value);
};
return (
<select onChange={handleChange}>
<option value="react">React</option>
<option value="vue">Vue</option>
</select>
);
}
Click events
import type { MouseEvent } from "react";
function Button() {
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget);
};
return <button onClick={handleClick}>Click me</button>;
}
Form events
import type { FormEvent } from "react";
function LoginForm() {
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("Form submitted");
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">Login</button>
</form>
);
}
Common React event types
| Event | Type |
|---|---|
| Input change | ChangeEvent<HTMLInputElement> |
| Textarea change | ChangeEvent<HTMLTextAreaElement> |
| Select change | ChangeEvent<HTMLSelectElement> |
| Button click | MouseEvent<HTMLButtonElement> |
| Form submission | FormEvent<HTMLFormElement> |
| Keyboard event | KeyboardEvent<HTMLInputElement> |
You usually don't need to memorize every event type. TypeScript autocomplete and your editor can help.
19. Typing useState
useState is one of the most common React hooks.
TypeScript can infer the state type from the initial value.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
TypeScript understands that count is a number.
Therefore:
setCount(10);
is valid, but:
setCount("Ten");
is not.
Explicit state types
Sometimes the initial value doesn't tell TypeScript enough.
For example:
const [user, setUser] = useState<User | null>(null);
Here:
type User = {
id: number;
name: string;
};
The state can contain either a User or null.
Later:
setUser({
id: 1,
name: "John",
});
State with multiple possible values
type Status = "idle" | "loading" | "success" | "error";
const [status, setStatus] = useState<Status>("idle");
Now only those four values are allowed.
Array state
interface User {
id: number;
name: string;
}
const [users, setUsers] = useState<User[]>([]);
You can update it:
setUsers((previousUsers) => [
...previousUsers,
{
id: 1,
name: "John",
},
]);
Important
Don't add explicit types unnecessarily.
This is usually enough:
const [count, setCount] = useState(0);
But this is useful:
const [user, setUser] = useState<User | null>(null);
20. Typing useEffect
useEffect is used to synchronize a component with external systems, such as APIs, subscriptions, or browser features.
TypeScript usually infers the types inside useEffect.
import { useEffect, useState } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
return <p>{count}</p>;
}
No additional type annotation is necessary.
Cleanup functions
import { useEffect } from "react";
function Timer() {
useEffect(() => {
const timer = setInterval(() => {
console.log("Tick");
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return <p>Timer running</p>;
}
TypeScript understands the cleanup function automatically.
The important lesson: you usually don't need to manually type useEffect.
21. Typing useRef
useRef is commonly used to access DOM elements or store values that persist between renders.
Accessing an input element
import { useRef } from "react";
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleFocus}>
Focus input
</button>
</div>
);
}
Let's understand this:
useRef<HTMLInputElement>(null)
The reference points to an HTML input element.
Initially, it is null because the DOM element hasn't been attached yet.
That's why the type is effectively:
HTMLInputElement | null
The optional chaining operator:
inputRef.current?.focus();
checks whether current exists before calling focus().
Other DOM elements
useRef<HTMLDivElement>(null);
useRef<HTMLButtonElement>(null);
useRef<HTMLTextAreaElement>(null);
Storing values
const countRef = useRef<number>(0);
This stores a number without causing a component re-render when it changes.
22. Typing useReducer
useReducer is useful when state logic becomes more complex.
import { useReducer } from "react";
type State = {
count: number;
};
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
const initialState: State = {
count: 0,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return {
count: state.count + 1,
};
case "decrement":
return {
count: state.count - 1,
};
case "reset":
return initialState;
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<h2>{state.count}</h2>
<button onClick={() => dispatch({ type: "increment" })}>
Increment
</button>
<button onClick={() => dispatch({ type: "decrement" })}>
Decrement
</button>
<button onClick={() => dispatch({ type: "reset" })}>
Reset
</button>
</div>
);
}
Notice this:
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" };
This is a discriminated union.
The type property identifies which action is being handled.
TypeScript can understand which action is active inside each case.
If you try:
dispatch({ type: "delete" });
TypeScript will report an error.
23. Typing Context API
Context allows components to share values without passing props through every level.
import {
createContext,
useContext,
useState,
} from "react";
interface User {
id: number;
name: string;
}
interface AuthContextType {
user: User | null;
login: (user: User) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(
undefined
);
Now define the provider:
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = (user: User) => {
setUser(user);
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
Create a custom hook:
function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}
Now components can access the context:
function Profile() {
const { user, logout } = useAuth();
if (!user) {
return <p>Please log in</p>;
}
return (
<div>
<h2>{user.name}</h2>
<button onClick={logout}>
Logout
</button>
</div>
);
}
The context type guarantees that consumers receive the correct structure.
24. Typing Custom Hooks
Custom hooks are reusable functions that use React hooks.
TypeScript makes custom hooks safer and easier to reuse.
import { useState } from "react";
function useToggle(initialValue: boolean = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => {
setValue((previousValue) => !previousValue);
};
return {
value,
toggle,
};
}
Usage:
function App() {
const { value, toggle } = useToggle();
return (
<button onClick={toggle}>
{value ? "ON" : "OFF"}
</button>
);
}
TypeScript infers the return type of the hook.
You don't need to manually write it unless there's a specific reason.
Custom hook with generics
import { useState } from "react";
function useArray<T>(initialValue: T[]) {
const [items, setItems] = useState<T[]>(initialValue);
const addItem = (item: T) => {
setItems((previousItems) => [...previousItems, item]);
};
const removeItem = (index: number) => {
setItems((previousItems) =>
previousItems.filter((_, i) => i !== index)
);
};
return {
items,
addItem,
removeItem,
};
}
Usage:
const { items, addItem } = useArray<string>(["React", "TypeScript"]);
addItem("Next.js");
This hook works with different data types:
const numbers = useArray<number>([1, 2, 3]);
numbers.addItem(4);
25. Typing Forms
Forms are an essential part of React applications.
Let's create a login form.
import { useState } from "react";
import type { ChangeEvent, FormEvent } from "react";
interface LoginForm {
email: string;
password: string;
}
function Login() {
const [form, setForm] = useState<LoginForm>({
email: "",
password: "",
});
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setForm((previousForm) => ({
...previousForm,
[name]: value,
}));
};
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
/>
<input
name="password"
type="password"
value={form.password}
onChange={handleChange}
/>
<button type="submit">
Login
</button>
</form>
);
}
The LoginForm interface defines the structure of the form state.
The event handler receives a React change event.
The submit handler receives a React form event.
A note about dynamic property updates
This expression:
[name]: value
uses a computed property name.
It allows us to update the field whose name matches the input.
For more complex forms, libraries such as React Hook Form can provide additional structure and validation.
26. Typing API Requests
One of the most important real-world TypeScript use cases is working with APIs.
Imagine an API returns this JSON:
{
"id": 1,
"name": "John",
"email": "john@example.com"
}
Create a type:
interface User {
id: number;
name: string;
email: string;
}
Then use it with Axios:
import axios from "axios";
import { useEffect, useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
async function fetchUser() {
try {
const response = await axios.get<User>(
"https://api.example.com/users/1"
);
setUser(response.data);
} catch (error) {
console.error(error);
}
}
fetchUser();
}, []);
if (!user) {
return <p>Loading...</p>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
This part is important:
axios.get<User>(...)
The generic tells Axios what type you expect the response data to have.
However, this is only a compile-time expectation.
TypeScript does not verify that the server actually returned a valid User.
If the API returns unexpected data, TypeScript won't automatically detect it at runtime.
For runtime validation, libraries such as Zod can help.
Typing fetch
interface User {
id: number;
name: string;
}
async function getUser(): Promise<User> {
const response = await fetch("/api/user");
if (!response.ok) {
throw new Error("Failed to fetch user");
}
const data: User = await response.json();
return data;
}
This example describes the expected type, but it still doesn't validate the JSON received from the server.
For production applications, consider validating external data at runtime.
27. Utility Types
TypeScript includes built-in utility types that help transform existing types.
These are especially useful in React projects.
Partial
Partial<T> makes every property optional.
interface User {
id: number;
name: string;
email: string;
}
type UpdateUser = Partial<User>;
Now:
const update: UpdateUser = {
name: "Sarah",
};
This is useful for update forms and partial updates.
Pick
Pick<T, Keys> selects specific properties.
type UserPreview = Pick<User, "id" | "name">;
Result:
type UserPreview = {
id: number;
name: string;
};
Omit
Omit<T, Keys> removes selected properties.
type UserWithoutEmail = Omit<User, "email">;
Readonly
Readonly<T> makes properties readonly.
type ReadonlyUser = Readonly<User>;
Now:
const user: ReadonlyUser = {
id: 1,
name: "John",
email: "john@example.com",
};
// TypeScript error
user.name = "Sarah";
Record
Record<Keys, Value> creates an object type with specific key and value types.
type UserRoles = Record<string, string>;
Example:
const roles: UserRoles = {
john: "admin",
sarah: "editor",
};
Another example:
type Status = "success" | "error" | "loading";
type StatusMessages = Record<Status, string>;
const messages: StatusMessages = {
success: "Operation completed",
error: "Something went wrong",
loading: "Please wait",
};
ReturnType
ReturnType<T> extracts the return type of a function.
function createUser() {
return {
id: 1,
name: "John",
};
}
type CreatedUser = ReturnType<typeof createUser>;
This is useful when you want to reuse a function's return type.
28. Enums vs. Literal Unions
TypeScript supports enums.
enum Status {
Loading,
Success,
Error,
}
Usage:
const currentStatus = Status.Loading;
But in many React applications, literal unions are a simpler alternative.
type Status = "loading" | "success" | "error";
Usage:
const currentStatus: Status = "loading";
Literal unions are often preferred because:
- They are simple.
- They work naturally with string values.
- They don't require an enum declaration.
- They are convenient for component props and state.
Enums are still useful in certain situations, but you don't need to use them everywhere.
29. any, unknown, and never
These three types are important to understand.
any
let value: any = "Hello";
value.toUpperCase();
value = 100;
value.someRandomMethod();
TypeScript stops checking many operations involving any.
This can hide bugs.
Avoid using any unless you have a specific reason.
unknown
unknown is a safer alternative to any.
let value: unknown = "Hello";
You cannot use the value freely without checking its type.
if (typeof value === "string") {
console.log(value.toUpperCase());
}
This makes unknown useful when handling data whose type is not known yet.
For example:
function handleError(error: unknown) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
This is a good pattern for handling errors in TypeScript.
never
never represents a value that should never occur.
One common use is exhaustive checking.
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
switch (status) {
case "loading":
return "Loading...";
case "success":
return "Success!";
case "error":
return "Error!";
}
}
A helper for exhaustive checking:
function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
function handleStatus(status: Status) {
switch (status) {
case "loading":
return "Loading...";
case "success":
return "Success!";
case "error":
return "Error!";
default:
return assertNever(status);
}
}
If someone adds a new status and forgets to handle it, TypeScript can help detect the missing case.
30. Type Assertions
Sometimes you know more about a value than TypeScript does.
A type assertion tells TypeScript how you want to treat that value.
const value = document.getElementById("username") as HTMLInputElement;
Now TypeScript treats value as an input element.
But be careful.
A type assertion does not perform runtime validation.
It does not convert a value into another type.
For example:
const value = "123" as unknown as number;
This does not convert the string into a number.
The value is still a string at runtime.
Use assertions carefully
Assertions are sometimes necessary, but they can hide errors if used incorrectly.
Prefer proper type narrowing when possible.
31. Common TypeScript Mistakes in React
Mistake 1: Using any everywhere
Bad:
function UserCard({ user }: any) {
return <h2>{user.name}</h2>;
}
Better:
interface User {
id: number;
name: string;
}
interface UserCardProps {
user: User;
}
function UserCard({ user }: UserCardProps) {
return <h2>{user.name}</h2>;
}
Mistake 2: Overusing explicit types
Unnecessary:
const [count, setCount] = useState<number>(0);
Usually enough:
const [count, setCount] = useState(0);
TypeScript already knows the type.
Mistake 3: Forgetting null
Incorrect:
const [user, setUser] = useState<User>(null);
Correct:
const [user, setUser] = useState<User | null>(null);
Mistake 4: Confusing type annotations with runtime validation
const user: User = await response.json();
This does not validate the API response.
It only tells TypeScript to treat the value as User.
Mistake 5: Using React.FC everywhere
You may encounter:
const Button: React.FC<ButtonProps> = ({ children }) => {
return <button>{children}</button>;
};
This is valid.
But modern React with TypeScript doesn't require React.FC.
You can simply write:
function Button({ children }: ButtonProps) {
return <button>{children}</button>;
}
This is a common and straightforward approach.
32. Recommended Learning Path
You don't need to master every advanced TypeScript feature before starting React.
Focus on the following order:
Level 1 — Essential TypeScript
stringnumberboolean- Arrays
- Objects
- Type annotations
- Type inference
- Functions
typeinterface- Optional properties
Level 2 — React TypeScript
- Component props
- Props destructuring
- Children
- Optional props
- Event types
useStateuseEffectuseRef
Level 3 — Intermediate React TypeScript
- Union types
- Literal types
- Type narrowing
- Generics
- Context API
useReducer- Custom hooks
- API response types
Level 4 — Advanced TypeScript
- Utility types
- Discriminated unions
unknownnever- Type assertions
- Generic components
- Runtime validation
Final Thoughts
TypeScript isn't about adding complicated syntax to JavaScript.
It's about making your code easier to understand, safer to maintain, and more predictable as your application grows.
When working with React, the most important TypeScript concepts are:
- Typing component props
- Understanding objects and interfaces
- Working with union types
- Typing state correctly
- Handling React events
- Understanding generics
- Typing API responses
- Using utility types
- Avoiding unnecessary
any
You don't need to memorize every TypeScript feature.
Learn the fundamentals, practice them in React components, and gradually introduce more advanced concepts as your applications become more complex.
The goal isn't to write the most complicated types.
The goal is to write clear, maintainable, and reliable React code.
Happy coding! 🚀
What TypeScript concept was the most confusing when you started learning React?
Share your experience in the comments. Let's help each other become better developers.
Top comments (0)