Using TypeScript with React and Node.js

advanced
14 min

Using TypeScript with React and Node.js

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.

Setting Up

Modern toolchains ship TypeScript support out of the box:

bash
# 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 --init

The @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.

Typing React Components and Props

Type props with an interface and annotate the function parameter — no special component type needed:

tsx
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.

Typing Hooks

useState usually infers its type from the initial value; supply the type argument when the initial value does not tell the whole story:

tsx
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 practice

Event handlers use React's synthetic event types:

tsx
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.

Typing a Node.js Server

With Express, install the framework and its types, then annotate request bodies and response payloads:

typescript
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:

typescript
import { z } from "zod"; const CreateTodo = z.object({ title: z.string().min(1) }); type CreateTodoBody = z.infer<typeof CreateTodo>; // inferred from the schema

Sharing Types Across Frontend and Backend

The 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.

Running TypeScript in Node

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.

Key Takeaways

  • Type React props with interfaces; use literal unions for variants and React.ReactNode for children.
  • useState<T> needs an explicit argument only when the initial value underdetermines the type.
  • Express generics type params, bodies, and responses — but always validate at runtime, ideally with Zod.
  • Share API types between client and server for end-to-end safety.
  • Use 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.

Using TypeScript with React and Node.js - TypeScript | CodeYourCraft | CodeYourCraft