React projects in 2026 are written in TypeScript, not plain JavaScript. TypeScript adds a type system on top of the JavaScript you just learned in the previous tutorial — every variable, every function parameter, every component prop has a type that the compiler checks before your code even runs. It catches whole categories of bugs (typos, missing properties, wrong shapes) before they reach the browser, and it gives your editor the power to autocomplete almost everything.
This tutorial walks you through the TypeScript concepts you’ll see most often in React code. Like the JavaScript tutorial, every section has a hands-on example you can paste into the TypeScript Playground (typescriptlang.org/play) for instant feedback. Don’t just read — type the examples in, hover over the variables to see the inferred types, and try breaking things on purpose to see what the compiler complains about. That’s how you build intuition.
1. Why TypeScript?
In plain JavaScript, this code runs without complaint, but breaks at runtime:
function greet(name) {
return "Hello, " + name.toUpperCase();
}
greet(42); // 💥 Runtime error: name.toUpperCase is not a function
In TypeScript, the same code is caught before it ever runs:
function greet(name: string) {
return "Hello, " + name.toUpperCase();
}
greet(42); // ❌ Compile error: Argument of type 'number' is not assignable to parameter of type 'string'
The compiler is your first reviewer. It will save you from yourself many times a day.
Why this matters in React: React components have props — values passed in from a parent. TypeScript lets you describe exactly what props a component accepts, and the compiler prevents you from forgetting one or passing the wrong type. Same for state, event handlers, API responses, and everything else you’ll touch.
2. Type Annotations — Variables, Parameters, Return Types
A type annotation is a colon followed by a type, written after a name.
Variables:
const age: number = 25;
const name: string = "Alex";
const isHost: boolean = true;
You can usually omit annotations on variables if the value is right there — TypeScript infers the type. The two lines below are equivalent:
const score = 100; // inferred as number
const score: number = 100; // explicit, same effect
Function parameters and return types:
function double(n: number): number {
return n * 2;
}
Every parameter needs a type (the compiler can’t guess). The return type after the ) is optional — the compiler infers it from the return statement — but writing it explicitly is a good habit on exported functions, because it documents intent.
Arrow functions work the same way:
const triple = (n: number): number => n * 3;
Try in the Playground: type the function below, then call it with the wrong type and watch the editor underline the mistake:
function getInitials(fullName: string): string {
return fullName.split(" ").map(word => word[0]).join("");
}
console.log(getInitials("Sam Lee")); // "SL"
console.log(getInitials(42)); // ❌ try this, see the error
Why this matters in React: Every component you write will have parameter types (props) and return types (JSX, which TypeScript expresses as JSX.Element or React.ReactNode).
3. Interfaces and Type Aliases
When you have an object with several fields — like a user, a song, or a component’s props — you describe its shape with either an interface or a type alias.
Interface:
interface Song {
title: string;
artist: string;
durationSeconds: number;
}
const track: Song = {
title: "Mr. Brightside",
artist: "The Killers",
durationSeconds: 222,
};
Type alias (does the same thing, different keyword):
type Song = {
title: string;
artist: string;
durationSeconds: number;
};
Which should you use? For object shapes, they’re nearly interchangeable. The community convention in React projects is: use interface for the shape of component props and public data structures, use type for unions, intersections, and shorthand. Don’t lose sleep over the choice — both are valid.
Optional properties with ?:
interface Song {
title: string;
artist: string;
durationSeconds: number;
albumArtUrl?: string; // optional — may or may not be set
}
const track: Song = {
title: "Mr. Brightside",
artist: "The Killers",
durationSeconds: 222,
// albumArtUrl is fine to omit
};
Readonly properties:
interface Song {
readonly id: string; // can't be reassigned after creation
title: string;
}
Why this matters in React: A component’s props are an object, and you describe that object with an interface:
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ label, onClick, disabled }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
}
Now any caller that forgets label or onClick is rejected by the compiler before they hit save.
4. Union Types, Literal Types, and Narrowing
A union type says “this value is one of several types,” joined with the | pipe character.
let value: string | number;
value = "hello"; // ✅
value = 42; // ✅
value = true; // ❌ not in the union
Literal types are unions of specific values, often strings. Hugely common in React for things like agent roles or output formats:
type AgentRole = "planner" | "researcher" | "synthesizer" | "script-writer";
function dispatchAgent(role: AgentRole) {
console.log(`Dispatching ${role} agent`);
}
dispatchAgent("planner"); // ✅
dispatchAgent("orchestrator"); // ❌ not in the literal union
The compiler’s autocomplete will offer you the four valid values when you type the function call. This is a massive productivity win and a near-magical feature when you first see it.
Narrowing. When a value’s type is a union, you can’t just call any method on it — TypeScript wants you to narrow down which branch you’re in:
function describe(value: string | number): string {
if (typeof value === "string") {
// Inside this branch, TypeScript knows value is a string.
return value.toUpperCase();
}
// Outside, it knows value is a number.
return value.toFixed(2);
}
The typeof check narrows the type. Other narrowing tools include if (value) (truthiness), Array.isArray(), and instanceof. The compiler’s awareness of narrowing is one of the things that makes TypeScript feel smart.
Why this matters in React: Loading states, form values, API responses — they’re all naturally unions. type LoadingState = "idle" | "loading" | "success" | "error" is one of the most common patterns you’ll write.
5. Generics — The <T> Magic
Generics let a function or type work with any type, as long as it’s used consistently. The most important place beginners meet generics is React’s useState hook.
import { useState } from "react";
const [count, setCount] = useState<number>(0);
// ^^^^^^^^
// This is a generic — the type goes inside <>
Read it as: “useState configured for numbers, initialized to 0.” Now count is typed as number and setCount only accepts number arguments.
setCount(count + 1); // ✅
setCount("five"); // ❌
For arrays, the most-typed-thing-ever in React is Array<T> (or its shorthand T[]):
const [songs, setSongs] = useState<Array<Song>>([]);
// or equivalently:
const [songs, setSongs] = useState<Song[]>([]);
Both forms mean “a list of Song objects.” Use whichever reads better at the call site; Song[] is more common.
Generic functions — slightly more advanced but you’ll see them:
function firstItem<T>(items: T[]): T | undefined {
return items[0];
}
const songs: Song[] = [/* ... */];
const top = firstItem(songs); // T is inferred as Song; top is Song | undefined
The <T> is a placeholder that gets filled in by the actual array’s type at the call site. You almost never have to write generic functions yourself in beginner-level React work, but you’ll use them constantly (every React hook is generic).
Why this matters in React: Every useState, useRef, useReducer, and most third-party hooks (RTK Query especially) take a generic type parameter. Being fluent at reading <T> syntax unlocks understanding of practically every React example you’ll find online.
6. Function and Event Types in React
React event handlers are typed too. The first time you see one, the type looks intimidating; once you know the pattern, it’s everywhere.
function SearchBox() {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log(event.target.value);
}
return <input onChange={handleChange} />;
}
The pattern:
React.ChangeEvent— the React event type<HTMLInputElement>— the kind of DOM element it came from
For different events, the type changes:
| Event | Type |
|---|---|
| Input change | React.ChangeEvent<HTMLInputElement> |
| Form submit | React.FormEvent<HTMLFormElement> |
| Button click | React.MouseEvent<HTMLButtonElement> |
| Key press | React.KeyboardEvent<HTMLInputElement> |
The shortcut: in modern React, you can usually skip the explicit type if you write the handler inline, because TypeScript infers from the JSX context:
return <input onChange={(e) => console.log(e.target.value)} />;
// ^ already typed correctly via inference
For function-shaped props on your own components, the simplest way is an arrow type:
interface ButtonProps {
onClick: () => void; // takes nothing, returns nothing
onSelect: (id: string) => void; // takes a string, returns nothing
}
Why this matters in React: Forms, buttons, dropdowns, drag-and-drop, keyboard shortcuts — every interactive piece of UI involves an event handler with a typed event object. You’ll write hundreds.
7. Utility Types You’ll Use Weekly — Partial, Pick, Omit, Record
TypeScript ships with built-in utility types — generic helpers for transforming other types. Four of them appear so often you should learn them now.
Partial<T> — make every property optional:
interface Song {
title: string;
artist: string;
durationSeconds: number;
}
type SongUpdate = Partial<Song>;
// equivalent to: { title?: string; artist?: string; durationSeconds?: number }
function updateSong(id: string, changes: SongUpdate) { /* ... */ }
updateSong("song-1", { title: "New Title" }); // ✅ only updating one field
Pick<T, K> — keep only the listed keys:
type SongPreview = Pick<Song, "title" | "artist">;
// equivalent to: { title: string; artist: string }
Omit<T, K> — remove the listed keys:
type SongWithoutDuration = Omit<Song, "durationSeconds">;
// equivalent to: { title: string; artist: string }
Record<K, V> — build an object type with keys of one type and values of another:
type AgentColors = Record<AgentRole, string>;
// equivalent to: { planner: string; researcher: string; synthesizer: string; "script-writer": string }
const colors: AgentColors = {
planner: "#4f46e5",
researcher: "#10b981",
synthesizer: "#f59e0b",
"script-writer": "#ef4444",
};
Why this matters in React: Forms typically need Partial<T> for “what the user has filled in so far.” Card components need Pick<T, ...> for “I only display these three fields.” API responses often need Omit<T, "internal_id"> to drop server-only fields. Record<K, V> builds lookup tables for things like color themes per agent role in your trace UI.
8. How to Read a TypeScript Error
TypeScript’s error messages are intimidating at first but follow a predictable structure. Once you can decode one, you can decode them all.
Example error from VS Code:
Argument of type '{ title: string; }' is not assignable to parameter of type 'Song'.
Property 'artist' is missing in type '{ title: string; }' but required in type 'Song'.
Read it line by line:
- “Argument of type X is not assignable to parameter of type Y” — you tried to pass an X where a Y was expected.
- “Property ‘artist’ is missing in type X but required in type Y” — the specific reason. You forgot to include
artist.
The fix: add the missing property.
A second common pattern:
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
This means you have a value that might be undefined (often from ? optional properties or array access), and you’re trying to use it where a definite string is required. The fix is usually to narrow (check it’s not undefined) before using it.
The discipline: when you see a TypeScript error, read it slowly. The compiler usually tells you exactly what’s wrong and what it expected. With practice, you’ll diagnose 80% of errors in under 10 seconds.
Why this matters: TypeScript errors are not the compiler nagging you. They’re the compiler doing your debugging for you, before runtime, on a free 24/7 plan. Befriend them.
Practice exercises
Paste each of the following into the TypeScript Playground. Predict what the compiler says before you click Run. Then fix the errors.
// 1. Type annotations
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, "3")); // What error appears? Fix it.
// 2. Interfaces
interface Run {
topic: string;
status: "planning" | "researching" | "synthesizing" | "complete";
startedAt: Date;
}
const myRun: Run = {
topic: "AI safety in 2026",
status: "researching",
};
// What's missing? Add it.
// 3. Generics
import { useState } from "react"; // pretend this works in the Playground
const [count, setCount] = useState<number>(0);
setCount("five"); // Why is this wrong?
// 4. Utility types
interface Song {
title: string;
artist: string;
durationSeconds: number;
albumArtUrl: string;
}
type SongPreview = Pick<Song, "title" | "artist">;
const preview: SongPreview = {
title: "Mr. Brightside",
artist: "The Killers",
durationSeconds: 222, // Why is this rejected?
};
// 5. Reading errors
function getFirstChar(s: string): string {
return s[0];
}
const result = getFirstChar(undefined); // What error appears?
Cheat sheet (keep this open while you code)
| Concept | Syntax |
|---|---|
| Variable annotation | const x: number = 5; |
| Function annotation | function f(x: number): string { ... } |
| Interface | interface Song { title: string; } |
| Type alias | type Song = { title: string; }; |
| Optional property | interface X { prop?: number; } |
| Readonly property | interface X { readonly id: string; } |
| Union type | type Status = "idle" | "loading" | "done"; |
| Generic state | useState<number>(0) |
| Generic array | Song[] or Array<Song> |
| Function type | (x: string) => boolean |
Partial<T> | every prop optional |
Pick<T, K> | keep only listed keys |
Omit<T, K> | remove listed keys |
Record<K, V> | object with K-typed keys, V-typed values |
What’s next
You’ve now seen the TypeScript surface area you’ll meet in 90% of React code. The deep parts (conditional types, mapped types, template literal types) are real but rare and you’ll learn them on the job if you ever need them.
If you want one more hands-on practice round, work through chapters 1–4 of the official TypeScript Handbook (typescriptlang.org/docs/handbook/). Otherwise: see you on Day 1.
Happy coding!