tsconfig.json, typing Express handlers and middleware, interfaces vs types, generics, ts-node for dev, tsc for prod — and migrating a JavaScript project incrementally.
Module P-3 — TypeScript in Node.js
What this module covers: TypeScript catches an entire class of bugs at compile time that JavaScript silently ships to production. This module covers setting up TypeScript for a Node.js API, the
tsconfig.jsonsettings that actually matter, typing Express handlers and middleware correctly, the type utilities you will use daily, and how to migrate an existing JavaScript project incrementally without stopping all other work.
Why TypeScript on the Backend
TypeScript's value is not just "autocomplete". It is a documentation system that the compiler enforces. Consider:
The TypeScript version:
- Documents what the function accepts — no need to read the implementation
- Errors at compile time if you pass
userIdas a string - Errors at the call site if you forget
items - Autocompletes
input.to show all available fields
At scale — hundreds of functions, dozens of developers, months of development — this prevents entire categories of bugs: wrong property names, missing required fields, null dereferences, wrong return type assumptions.
Installation and Setup
Install type definitions for your libraries:
tsconfig.json: The Settings That Matter
The single most important setting is "strict": true. It enables:
strictNullChecks—nullandundefinedare not assignable to other typesnoImplicitAny— variables must have explicit types when they can't be inferredstrictFunctionTypes— function parameter types are checked contravariantly- Several others
Without strict, TypeScript is considerably less useful. Always start with it on.
TypeScript Project Structure
Update package.json scripts:
tsc --noEmit type-checks without producing output — fast, use in CI.
Typing Your Domain Models
Define your core types once and import them everywhere:
Typing Express Handlers
Express's built-in types are usable but loose. Here is the correct pattern:
Augmenting the Express Request Type
The authenticate middleware from P-2 adds req.user — but TypeScript does not know that. Fix it with module augmentation:
After this, req.user is fully typed in every handler — no casting needed:
Interfaces vs Types
Both define object shapes. The differences that matter in practice:
Practical rule: use interface for object shapes (models, DTOs, service inputs). Use type for unions, intersections, and computed types (Omit, Pick, Partial).
Utility Types You Use Daily
Generics: Writing Flexible Typed Code
Generics let you write functions and types that work with any type while preserving type information:
Typing Service and Repository Layers
The type information flows from repository → service → controller. TypeScript catches mismatches at every boundary.
Incrementally Migrating JavaScript to TypeScript
You do not need to convert everything at once. The incremental approach:
Step 1: Add TypeScript without breaking anything
Step 2: Convert files one by one
Rename .js to .ts. Fix errors. Commit. Move to the next file.
Start with:
types/models.ts— define your domain types firstrepositories/— add return types to DB functionsservices/— add input/output typescontrollers/— typereq,res,next- Last:
index.ts, middleware, routes
Step 3: Tighten the config progressively
Step 4: Enable noUncheckedIndexedAccess
This is the last setting to add — it causes the most friction but catches real bugs:
Path Aliases
Avoid ../../../repositories/users.repository with path aliases:
For ts-node to resolve these, also install:
Summary
"strict": trueis non-negotiable. It enables the checks that make TypeScript worth using.module: "NodeNext"withmoduleResolution: "NodeNext"for correct ESM + CJS interop in Node.js.- Type your domain models in
src/types/models.tsand import them everywhere — single source of truth. - Augment
Express.Requestinsrc/types/express.d.tsto typereq.user,req.requestId, and any other middleware-added properties. - Use
interfacefor object shapes,typefor unions and computed types. Both are fine — consistency matters more than which you pick. - Utility types (
Omit,Pick,Partial,Readonly,ReturnType) reduce duplication and keep types in sync with their source. - Migrate incrementally —
allowJs: truelets TypeScript and JavaScript coexist. Convert file by file starting with the types and data access layers.
Next: input validation and error handling — replacing manual if (!name) checks with Zod schemas and building the error pipeline that makes every handler clean.
When migrating a Node.js project to TypeScript, which compiler option allows TypeScript and JavaScript files to coexist without strictly type-checking the JS files?
What is the primary practical difference between interface and type in TypeScript?
Which TypeScript utility type would you use to create a new type representing a User model without the passwordHash field?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion