Working with TypeScript

1. Installing TypeScript Dependencies

Example: Install

npm i express
npm i -D typescript tsx @types/node @types/express

2. Creating tsconfig.json Configuration

Example: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*"]
}

3. Typing Request and Response

Example: Typed handler

import type { Request, Response, NextFunction } from "express";

export const ping = (req: Request, res: Response) => {
  res.json({ ok: true });
};

4. Creating Custom Request Types

Example: Module augmentation

// src/types/express.d.ts
import "express";
declare module "express-serve-static-core" {
  interface Request {
    id: string;
    user?: { sub: string; role: "admin" | "user"; email: string };
  }
}

5. Typing Middleware Functions

Example: RequestHandler

import type { RequestHandler } from "express";

export const requireAuth: RequestHandler = (req, res, next) => {
  if (!req.user) return next(new HttpError(401));
  next();
};

6. Using Generic Types

Example: Request<Params, ResBody, ReqBody, Query>

import type { Request, Response } from "express";

type Params = { id: string };
type Body   = { email: string };
type Resp   = { id: string; email: string };
type Query  = { include?: string };

export const updateUser = async (
  req: Request<Params, Resp, Body, Query>,
  res: Response<Resp>
) => {
  const user = await db.users.update(req.params.id, req.body);
  res.json(user);
};

7. Creating Type-Safe Route Handlers

Example: Wrapper

import { z, ZodSchema } from "zod";

export const handler = <P, B, Q, R>(
  schemas: { params?: ZodSchema<P>; body?: ZodSchema<B>; query?: ZodSchema<Q> },
  fn: (input: { params: P; body: B; query: Q }) => Promise<R>
): RequestHandler => async (req, res, next) => {
  try {
    const input = {
      params: schemas.params ? schemas.params.parse(req.params) : ({} as P),
      body:   schemas.body   ? schemas.body.parse(req.body)     : ({} as B),
      query:  schemas.query  ? schemas.query.parse(req.query)   : ({} as Q)
    };
    res.json(await fn(input));
  } catch (err) { next(err); }
};

8. Typing Express Application

Example: Express type

import express, { type Express, type Router } from "express";

export function createApp(): Express {
  const app = express();
  app.use(express.json());
  return app;
}

9. Using Type Guards

Example: Narrow error

export class HttpError extends Error {
  constructor(public status: number, msg = "") { super(msg); }
}

export function isHttpError(e: unknown): e is HttpError {
  return e instanceof HttpError;
}

10. Compiling TypeScript

ToolUse
tscType-check + emit JS
tsxDev runner (no emit) RECOMMENDED
ts-nodeOlder alternative
swc / esbuildFast transpile (no type-check)
Node 22.6+ --experimental-strip-typesRun .ts directly (preview)

Example: package.json scripts

{
  "scripts": {
    "dev":   "tsx watch --env-file=.env src/server.ts",
    "build": "tsc -p tsconfig.json",
    "start": "node --enable-source-maps dist/server.js",
    "typecheck": "tsc --noEmit"
  }
}