TypeScript with Node.js
TypeScript adds static typing to JavaScript, catching errors at compile time rather than runtime. Think of it as a spell-checker for your code: just as a spell-checker catches typos before you send an email, TypeScript catches type errors before your code runs in production.
For Node.js applications, TypeScript provides better IDE support (autocomplete that actually knows what properties an object has), safer refactoring (rename a function and the compiler tells you every call site that needs updating), and self-documenting code (the types themselves serve as documentation that cannot drift out of date).
Why TypeScript?
Project Setup
Setting up TypeScript in a Node.js project requires a few extra dependencies compared to plain JavaScript. The typescript package is the compiler itself. ts-node lets you run .ts files directly without a separate compile step (essential for development). The @types/* packages provide type definitions for libraries written in plain JavaScript — they are the Rosetta Stone that teaches TypeScript what express, node, and other libraries look like.
tsconfig.json
The tsconfig.json file is the control panel for the TypeScript compiler. Each option below is annotated with why it matters.
Package.json Scripts
Node.js tip: In production, always run the compiled JavaScript (node dist/server.js), never ts-node directly. ts-node adds startup overhead and memory usage from the TypeScript compiler. Your CI/CD pipeline should run npm run build and deploy only the dist/ folder.
Basic Types
TypeScript’s type system builds on JavaScript’s existing types but makes them explicit and enforced. In plain JavaScript, a variable can silently change from a number to a string to an object — TypeScript prevents that. Think of types as contracts: when you declare age: number, you are promising that age will always be a number, and the compiler holds you to it.
Interfaces and Types
TypeScript offers two ways to define the shape of data: interface and type. The difference is mostly stylistic for simple objects, but there are real differences. Interfaces are extendable (you can add fields to an existing interface by declaring it again — this is called “declaration merging”), while types are better for unions, intersections, and computed types. The Node.js/Express ecosystem convention is to use interface for object shapes and type for everything else.
Express with TypeScript
Basic Server Setup
Typed Request Handlers
One of the most powerful aspects of TypeScript with Express is the ability to type your request parameters, body, and response. Express’s Request type accepts generic parameters: Request<Params, ResBody, ReqBody, Query>. By filling these in, your handler gets autocomplete and type checking for req.params, req.body, and res.json().
Typed Routes
Zod for Runtime Validation
Here is a subtlety that trips up TypeScript beginners: TypeScript types exist only at compile time. They are completely erased when your code runs. This means TypeScript cannot validate data that arrives at runtime — like HTTP request bodies, environment variables, or API responses from external services. The data could be anything.
Zod bridges this gap. It lets you define a schema that validates data at runtime AND automatically infers a TypeScript type from that schema. One definition, both compile-time types and runtime validation. No duplication, no drift between your types and your validation logic.
Error Handling
TypeScript makes error handling more robust by letting you define typed error hierarchies. The public readonly modifiers in the constructor are a TypeScript shorthand that simultaneously declares the property on the class and assigns the constructor argument to it — avoiding the repetitive this.statusCode = statusCode pattern.
Database with Prisma
Prisma is the ideal database tool for TypeScript projects because it generates a fully-typed client from your schema file. Every query, every result, every relation is type-checked at compile time. If you rename a column in your schema and forget to update a query, TypeScript catches it immediately. This level of type safety is simply not possible with raw SQL or loosely-typed ORMs.
Authentication Middleware
TypeScript shines in authentication middleware because it catches common mistakes like accessing properties that do not exist on the decoded token. The as JwtPayload type assertion below tells TypeScript the shape of the decoded token — if you later change the token structure, the types will guide you to update every consumer.
Testing with TypeScript
Testing TypeScript code with Jest requires a small amount of configuration. Install ts-jest to let Jest understand .ts files, or use @swc/jest for faster compilation. The key benefit: your test code is also type-checked, so typos in test assertions are caught at compile time rather than producing confusing test failures.
Project Structure
A well-organized TypeScript project separates concerns into clearly-named directories. This structure mirrors the MVC (Model-View-Controller) pattern adapted for APIs: schemas handle input validation, services contain business logic, controllers handle HTTP concerns, and routes wire everything together. The types/ directory holds custom type definitions, and utils/ contains shared helpers.
Summary
- TypeScript adds static typing to Node.js
- Use strict mode for maximum type safety
- Zod provides runtime validation with type inference
- Prisma offers type-safe database access
- Extend Express types for custom request properties
- Create custom error classes for typed error handling
- Use generics for reusable, type-safe code
- Configure path aliases for cleaner imports