TypeScript is JavaScript with types. A type says what kind of value something is: a number, a piece of text, a list of users. TypeScript checks this while you write, and underlines mistakes in your editor before you run the app. Many React jobs ask for TypeScript, because it catches many bugs early and makes big projects easier to understand. This lesson shows the parts you need for React, with plain language.
Why use TypeScript?
- It finds typos and wrong values as you type, such as
user.nmaeor passing a string where a number is needed. - It gives better auto-complete in your editor, because it knows the shape of your data.
- It works as living documentation: you can see what props a component needs without opening it.
- It makes changing code safer, because the editor shows every place that must change.
The price is a little extra writing and a short time to learn. TypeScript is removed when your app is built, so the browser still runs plain JavaScript. It does not make code run faster or slower.
Start a TypeScript project
npm create vite@latest my-ts-app -- --template react-ts
cd my-ts-app
npm install
npm run dev
Files now end in .ts (plain code) and .tsx (code with JSX). Use .tsx for components.
The basic types
const name: string = 'Asha';
const age: number = 24;
const isAdmin: boolean = false;
const tags: string[] = ['react', 'css']; // a list of text
const scores: Array<number> = [1, 2, 3]; // the same idea
const pair: [string, number] = ['age', 24]; // a tuple: exact shape
let id: string | number = 5; // a union: one or the other
id = 'abc'; // fine
function double(n: number): number {
return n * 2;
}
TypeScript can often guess a type, so you do not always write it. const age = 24 is already known to be a number. Write types where TypeScript cannot guess, such as function parameters.
Describing objects: type and interface
type User = {
id: number;
name: string;
email: string;
phone?: string; // the ? means optional
role: 'admin' | 'editor' | 'viewer'; // only these words are allowed
};
const user: User = { id: 1, name: 'Asha', email: 'a@b.com', role: 'admin' };
// user.role = 'boss'; // error: not one of the allowed words
interface is almost the same and you will see both. A simple rule: use type or interface consistently in your project. The union of allowed words, like 'admin' | 'editor', is very useful for statuses and options.
Typing props
type ButtonProps = {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: 'primary' | 'secondary';
};
function Button({ label, onClick, disabled = false, variant = 'primary' }: ButtonProps) {
return (
<button className={variant} onClick={onClick} disabled={disabled}>
{label}
</button>
);
}
// <Button label="Save" onClick={save} /> fine
// <Button label="Save" /> error: onClick is missing
// <Button label={5} onClick={save} /> error: label must be text
Now the editor tells you exactly which props a component needs and what type each should be, with no need to open the file.
For components that take children, use ReactNode:
import type { ReactNode } from 'react';
type CardProps = {
title: string;
children: ReactNode;
};
function Card({ title, children }: CardProps) {
return <section><h2>{title}</h2>{children}</section>;
}
Typing state
useState often guesses the type from the first value. When the first value does not show the full type, tell it with angle brackets (a generic):
const [count, setCount] = useState(0); // number, guessed
const [name, setName] = useState(''); // string, guessed
const [user, setUser] = useState<User | null>(null); // a User, or nothing yet
const [items, setItems] = useState<string[]>([]); // an empty list of text
useState<User | null>(null) says: this may hold a user or null. Now TypeScript makes you check for null before using it, which prevents the very common error “cannot read property of null”:
if (!user) return <p>Loading...</p>;
return <h1>{user.name}</h1>; // safe: TypeScript knows user is a User here
Typing events
import type { ChangeEvent, FormEvent } from 'react';
function Form() {
const [email, setEmail] = useState('');
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setEmail(event.target.value);
}
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log(email);
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={handleChange} />
</form>
);
}
You do not have to remember these names. If you write the handler inline, TypeScript knows the type already: onChange={(e) => setEmail(e.target.value)}. You only need a name when you write the handler as a separate function. Your editor will tell you the right one when you hover over the prop.
Typing refs
import { useRef } from 'react';
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current?.focus(); // ?. because it could be null
}
<input ref={inputRef} />
Typing API data
Describe what you expect from the server, then use that type when you fetch:
type Post = { id: number; title: string; body: string };
async function fetchPosts(): Promise<Post[]> {
const response = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
if (!response.ok) throw new Error('Request failed');
return response.json();
}
function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
fetchPosts().then(setPosts);
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
Typing context and custom hooks
type AuthContextValue = {
user: User | null;
login: (name: string) => void;
logout: () => void;
};
const AuthContext = createContext<AuthContextValue | null>(null);
export function useAuth(): AuthContextValue {
const value = useContext(AuthContext);
if (!value) throw new Error('useAuth must be used inside an AuthProvider');
return value;
}
// a generic custom hook
export function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const saved = localStorage.getItem(key);
return saved ? (JSON.parse(saved) as T) : initial;
});
return [value, setValue] as const;
}
The T in useLocalStorage<T> is a generic: a placeholder for any type. useLocalStorage('count', 0) makes T a number, and useLocalStorage('user', { name: '' }) makes it an object. One hook, many types, all checked.
Useful tools to know
- `Partial<User>` makes all fields optional. Useful for updates.
- `Pick<User, 'id' | 'name'>` keeps only some fields, and `Omit<User, 'email'>` removes some.
- `Record<string, number>` is an object with text keys and number values.
- `as const` makes a value read-only and exact.
- `unknown` is a safe version of
any. It forces you to check the type before using it.
Avoid any
The type any turns off checking for that value. It is tempting when a type is hard, but each any removes the safety you came for. Use unknown and narrow it, or write a simple type. Turn on strict mode in tsconfig.json (Vite does it for you), so TypeScript is as helpful as possible.
Reading TypeScript errors
Errors can look long. Read the last line first, because it states the real difference, and read where (file and line). Hover the red line in your editor for a clearer explanation. Most errors in the first weeks are: a value that might be null, a missing prop, a wrong type in a prop, or a typo in a field name. All are good catches.
Common mistakes
- Using
anyto make errors go away. - Typing
useState([])with no type, which makes the listnever[]. WriteuseState<string[]>([]). - Forgetting that data from the network is not checked at runtime.
- Casting with
asto silence an error, hiding a real problem. - Not enabling strict mode.
A tiny TypeScript story
Here is why people like TypeScript, in a small story. A teammate renames the field name to fullName in the User type. In plain JavaScript, nothing tells you which of your thirty components used user.name. Some of them silently show undefined on the page, and you only find out when a visitor complains. In TypeScript, the moment the type changes, the editor underlines every place that still says user.name. You fix them in a few minutes, and nothing reaches your visitors.
That is the main benefit: mistakes show up while you write, not after you publish. It is a safety net for changing code, and the bigger the project, the more it helps. For a tiny practice app you may not need it, and that is fine. Many people learn React first and add TypeScript in the next project.
How to move from JavaScript step by step
- Start a new project with the
react-tstemplate, and write only simple types at first: props, and state with a generic. - Let TypeScript guess types where it can, and write types for function parameters and data from the network.
- Read each error calmly. They are hints about real mistakes.
- Add more advanced types, such as generics and utility types, only when you need them.
Your task
- Create a Vite React TypeScript project and make a
Buttoncomponent with typed props, including avariantunion. - Create a
Usertype, and aUserCardcomponent that takes auserprop. Try passing a wrong field and read the error. - Fetch posts from the sample API and type the result with a
Posttype. - Make a generic
useLocalStorage<T>hook and use it with a string and with an object.
How to check it worked
- The editor shows red lines when you pass a wrong prop, and the error message makes sense.
- Auto-complete shows the fields of your
Usertype. npm run buildruns with no TypeScript errors.
Summary
- TypeScript adds types to JavaScript and finds mistakes in your editor before you run the app.
- Type props with
typeorinterface, state withuseState<Type>(), and events withChangeEventand similar types. - Use unions for fixed options,
?for optional fields, and generics for reusable hooks. - Avoid
any, check network data at runtime, and keep strict mode on.