Curriculum1 of 6
+ Module 08

TypeScript for UI

Type component props and data confidently.

Phase 1 · JavaScript and React · 6 sections · about 3 minutes

+ 01 / 06

What Types Add

JavaScript is permissive. A function expects a number but gets a string. It runs anyway, crashes later, and you debug for an hour. TypeScript catches this at write-time. Your editor flags the mistake before you save.

Types are a contract. They say: "This prop is a string, always. This object has a name and an age. This function returns a boolean or null, never undefined." The computer enforces the contract.

+ JavaScript
1// JavaScript, no complaint until the crash
2function greet(name) {
3 return "Hello, " + name.toUpperCase();
4}
5 
6greet(42); // Crashes: 42 has no toUpperCase method
+ TypeScript
1// TypeScript, catches it immediately
2function greet(name: string) {
3 return "Hello, " + name.toUpperCase();
4}
5 
6greet(42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'

Types aren't just safety. They're a form of documentation. Six months later, you read function loadUser(id: number): Promise<User> and immediately understand what the function does and what it returns. No guessing.

+ 02 / 06

Structural Typing

TypeScript doesn't care about names. It cares about shape. If an object has the fields you need, TypeScript treats it as the right type. This is called structural typing, and it's the opposite of Java's nominal typing (where the name matters).

+ TypeScript
1// Two different type names, same shape
2type User = {
3 id: number;
4 name: string;
5};
6 
7type Person = {
8 id: number;
9 name: string;
10};
11 
12const user: User = { id: 1, name: "Alice" };
13const person: Person = user; // Valid. TypeScript doesn't care about the type name.

Toggle properties on and off and watch the only thing that decides whether this compiles: whether the required ones are present. Then rename the variable and watch nothing happen.

+ Try itassignable

TypeScript checks the shape, not the name

The type it must satisfy
type User = {
  id: number;
  name: string;
  email?: string;
  isAdmin?: boolean;
};
Properties on your object
Your object
const user: User = {
  id: 3,
  name: "...",
};
Compiles

Both required properties are here, so the object satisfies User.

Try it. A language with nominal typing would care what the thing is called. TypeScript does not.

Toggle properties on and off. The only thing that decides whether this compiles is whether the required properties are present.

+ TypeScript
1// This works because the object has the right fields
2type Author = {
3 name: string;
4 email: string;
5};
6 
7const author: Author = {
8 name: "Neil",
9 email: "neil@example.com",
10 bio: "A designer", // Extra fields are fine
11};

Structural typing makes TypeScript flexible. You don't need to inherit from a base class or implement an interface (though you can). You just need the right fields in the right shape. This is why a fetch response, a hardcoded object, and a database query result can all satisfy the same type.

+ 03 / 06

Typing Component Props

React components receive props. Props are objects. TypeScript makes sure the props you pass match what the component expects.

+ TypeScript
1// Define what props the button accepts
2type ButtonProps = {
3 label: string;
4 onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
5 disabled?: boolean; // Optional prop
6};
7 
8function Button({ label, onClick, disabled = false }: ButtonProps) {
9 return (
10 <button onClick={onClick} disabled={disabled}>
11 {label}
12 </button>
13 );
14}
15 
16// Usage
17<Button label="Save" onClick={() => console.log("saved")} />
18<Button label="Delete" onClick={handleDelete} disabled={true} />
19// <Button label="Save" onClick={console.log} />, Error: wrong callback signature

The ? marks a prop as optional. disabled = false sets its default. TypeScript checks both at the call site (when you use the component) and inside the component (the handler must match the type).

+ TypeScript
1// Common pattern: spread remaining props
2type CardProps = {
3 title: string;
4 children: React.ReactNode;
5} & React.HTMLAttributes<HTMLDivElement>;
6 
7function Card({ title, children, ...rest }: CardProps) {
8 return (
9 <div {...rest}>
10 <h2>{title}</h2>
11 {children}
12 </div>
13 );
14}

Typing props costs almost nothing. You get editor autocomplete, instant error messages, and documentation in one place. Every component in a typed codebase answers "what do I need?" without opening its body.

+ 04 / 06

Typing API Responses

Your component fetches data. The server sends JSON. TypeScript must understand the shape of that JSON or the data is worthless.

+ TypeScript
1// Before fetch, define the shape
2type BookData = {
3 id: string;
4 title: string;
5 wordCount: number;
6 chapters: {
7 number: number;
8 title: string;
9 content: string;
10 }[];
11};
12 
13async function loadBook(bookId: string): Promise<BookData> {
14 const response = await fetch(`/api/books/${bookId}`);
15 const data = await response.json();
16 return data as BookData; // Cast if you trust the server
17}
18 
19// Now TypeScript knows what's in the data
20const book = await loadBook("1");
21console.log(book.title); // Valid
22console.log(book.author); // Error: type has no 'author' property

Casting with as is a risky escape hatch. The server might send different data and the type lies. Safer: validate at runtime.

+ TypeScript
1// Runtime validation with a type guard
2function isBookData(data: unknown): data is BookData {
3 return (
4 typeof data === "object" &&
5 data !== null &&
6 typeof (data as any).id === "string" &&
7 typeof (data as any).title === "string" &&
8 typeof (data as any).wordCount === "number" &&
9 Array.isArray((data as any).chapters)
10 );
11}
12 
13async function loadBook(bookId: string): Promise<BookData> {
14 const response = await fetch(`/api/books/${bookId}`);
15 const data = await response.json();
16 if (!isBookData(data)) {
17 throw new Error("Invalid book data from server");
18 }
19 return data; // TypeScript now knows it's BookData, no cast needed
20}

Validation is tedious but safe. For large APIs, libraries like Zod automate it. For now, the principle is simple: never trust the server. Verify the shape before you use it.

+ 05 / 06

Build: Retype the Component

Take an existing component that fetches data and renders it. Add types everywhere: the component props, the API response, the rendered data.

Start with the API response. What does the server send? Write a type. Then the component props. What does the component need? Write a type. Finally, inside the component, rely on TypeScript to flag any mismatches.

+ TypeScript
1// Step 1: Define the API shape
2type ChapterData = {
3 id: string;
4 number: number;
5 title: string;
6 wordCount: number;
7};
8 
9type BookResponse = {
10 id: string;
11 title: string;
12 author: string;
13 chapters: ChapterData[];
14};
15 
16// Step 2: Define component props
17type BookViewerProps = {
18 bookId: string;
19 onChapterClick?: (chapterId: string) => void;
20};
21 
22// Step 3: Build the component with types
23function BookViewer({ bookId, onChapterClick }: BookViewerProps) {
24 const [book, setBook] = React.useState<BookResponse | null>(null);
25 const [loading, setLoading] = React.useState(true);
26 
27 React.useEffect(() => {
28 async function load() {
29 const response = await fetch(`/api/books/${bookId}`);
30 const data: BookResponse = await response.json();
31 setBook(data);
32 setLoading(false);
33 }
34 load();
35 }, [bookId]);
36 
37 if (loading) return <div>Loading...</div>;
38 if (!book) return <div>Not found</div>;
39 
40 return (
41 <div>
42 <h1>{book.title}</h1>
43 <p>By {book.author}</p>
44 <ul>
45 {book.chapters.map((ch) => (
46 <li key={ch.id} onClick={() => onChapterClick?.(ch.id)}>
47 {ch.number}: {ch.title} ({ch.wordCount} words)
48 </li>
49 ))}
50 </ul>
51 </div>
52 );
53}

Check the component in your editor. Is the state typed? Are the props required? Does the fetch response have all the fields you're using? TypeScript will tell you. Fix any red squiggles. When they're gone, the component is correct.

+ 06 / 06

Checkpoint

You've typed a data-backed component. The component props are explicit. The API response is verified. No any types snuck in. Your editor autocompletes .chapters[0].title because TypeScript knows the shape.

This is the foundation. From here, types scale to entire applications. A large codebase where every function, prop, and response is typed is a codebase where refactoring is safe and adding features is fast.

+ Up nextReact FundamentalsPreviouslyIntermediate JavaScript: Fetch and Real Data