Everything you have learned so far comes together when TypeScript meets real frameworks. This lesson shows the essential patterns for typing React components, hooks, and events, and for building type-safe Node.js servers — the two environments where most TypeScript is written today.
Modern toolchains ship TypeScript support out of the box:
# React with Vite
npm create vite@latest my-app -- --template react-ts
# Node.js API project
npm init -y
npm install --save-dev typescript tsx @types/node
npx tsc --initThe @types/node package provides type definitions for Node built-ins like fs and http. Many libraries bundle their own types; for older ones, install the community-maintained @types/<package> from DefinitelyTyped.
Type props with an interface and annotate the function parameter — no special component type needed:
interface ButtonProps {
label: string;
variant?: "primary" | "secondary";
onClick: () => void;
children?: React.ReactNode;
}
function Button({ label, variant = "primary", onClick, children }: ButtonProps) {
return (
<button className={"btn-" + variant} onClick={onClick}>
{label}
{children}
</button>
);
}Literal unions like "primary" | "secondary" give consumers autocomplete and reject typos. React.ReactNode is the correct type for anything renderable.
useState usually infers its type from the initial value; supply the type argument when the initial value does not tell the whole story:
const [count, setCount] = useState(0); // number, inferred
const [user, setUser] = useState<User | null>(null); // explicit union
const inputRef = useRef<HTMLInputElement>(null); // DOM ref
const [state, dispatch] = useReducer(reducer, initialState);
// dispatch is typed from the reducer action union — a discriminated union in practiceEvent handlers use React's synthetic event types:
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value); // typed as string
}Tip: write the handler inline first, hover to see the inferred event type, then extract it.
With Express, install the framework and its types, then annotate request bodies and response payloads:
import express, { Request, Response } from "express";
interface CreateTodoBody { title: string }
interface Todo { id: number; title: string; done: boolean }
const app = express();
app.use(express.json());
app.post(
"/todos",
(req: Request<{}, Todo, CreateTodoBody>, res: Response<Todo>) => {
const todo: Todo = { id: Date.now(), title: req.body.title, done: false };
res.status(201).json(todo);
}
);The generic parameters on Request type the route params, response body, and request body. Remember: types are erased at runtime, so still validate incoming data — libraries like Zod pair a runtime validator with an inferred static type, giving you both checks from one schema:
import { z } from "zod";
const CreateTodo = z.object({ title: z.string().min(1) });
type CreateTodoBody = z.infer<typeof CreateTodo>; // inferred from the schemaThe biggest payoff of full-stack TypeScript is one definition of your API contract. In a monorepo, put shared interfaces in a packages/shared workspace and import them from both sides; changing a field then produces compile errors in every affected client and handler. Tools like tRPC and OpenAPI generators automate this further.
For development, tsx watch src/server.ts restarts on save with zero config. For production, either compile with tsc and run the emitted JavaScript, or rely on Node's built-in type stripping in recent versions for simple projects.
React.ReactNode for children.useState<T> needs an explicit argument only when the initial value underdetermines the type.tsx in development; compile with tsc for production.Next lesson: TypeScript Best Practices and Common Mistakes — the habits that separate clean TypeScript from noisy TypeScript.