The short answer
Install @types/express, then check each named handler against the RequestHandler type it provides. In a JavaScript file, write that with @satisfies, not @type:
import express from "express";
const app = express();
/** @satisfies {import("express").RequestHandler<{ id: string }>} */
const getUser = (req, res) => {
res.json({ id: req.params.id, name: "Ada" });
};
app.get("/users/:id", getUser);
app.get("/orders/:orderId", (req, res) => {
res.json({ orderId: req.params.orderId });
});
app.listen(3000);import express, { type RequestHandler } from "express";
const app = express();
const getUser = ((req, res) => {
res.json({ id: req.params.id, name: "Ada" });
}) satisfies RequestHandler<{ id: string }>;
app.get("/users/:id", getUser);
app.get("/orders/:orderId", (req, res) => {
res.json({ orderId: req.params.orderId });
});
app.listen(3000);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} RequestHandler<{ id: string }> says the route has one parameter, id. The handler’s req and res take their types from it, so req.params.id is a string and a misspelled parameter is an error. The inline handler for /orders/:orderId needs no annotation at all: @types/express reads the parameter names out of the route string.
Install the types
Express itself is a normal dependency; its types and the checker are development dependencies. Version 5 of @types/express describes Express 5.
npm install express
npm install --save-dev typescript @types/express @types/nodeThe examples on this page use the Node.js ES modules setup with strict on.
Why @satisfies and not @type
The obvious spelling, @type, fails as soon as a handler does not return anything:
import express from "express";
const app = express();
/** @type {import("express").RequestHandler<{ id: string }>} */
const getUser = (req, res) => {error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
res.json({ id: req.params.id });
};
app.get("/users/:id", getUser);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} In a JavaScript file, @type on a function annotates the whole function, its return type included. RequestHandler declares its return type as unknown, so a block body with no return statement is held to “must return a value” and fails with TS2355. A type annotation on a variable in a .ts file only types the arrow function from its context, which is why the TypeScript version of the same code has no error.
@satisfies (TypeScript 5.0 and later) is the JSDoc tag that behaves like that annotation here: it checks the function against RequestHandler and gives its parameters their types, and the function keeps its own inferred return type. Two other spellings also work: separate @param tags for req and res (typed import("express").Request<{ id: string }> and import("express").Response), or keeping @type and writing return res.json(…).
TypeScript 7.0 reworked its checking of .js files to match .ts files, and TypeScript 7.0.2 does not report the @type handler above: its badge shows the difference, and the section Under TypeScript 6.0 and 7.0 explains it. @satisfies passes in every version, so it stays the spelling to use (TypeScript 7.0 for JavaScript users).
A parameter the route does not have
With the parameters typed, a name that is not in the type is caught before the request ever runs:
import express from "express";
const app = express();
/** @satisfies {import("express").RequestHandler<{ id: string }>} */
const getUser = (req, res) => {
res.json({ id: req.params.userId });error TS2339: Property 'userId' does not exist on type '{ id: string; }'.
};
app.get("/users/:id", getUser);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Typing the request body and the error handler
req.body is any until you say what it holds. RequestHandler takes the route parameters, the response body and the request body as its first three type parameters, in that order:
import express from "express";
/** @import { RequestHandler, ErrorRequestHandler } from "express" */
/**
* @typedef {object} NewUser
* @property {string} name
* @property {string} [email]
*/
/** @typedef {{ id: number, name: string }} CreatedUser */
/** @satisfies {RequestHandler<{}, CreatedUser, NewUser>} */
const createUser = (req, res) => {
const { name } = req.body;
res.status(201).json({ id: Date.now(), name });
};
/** @satisfies {ErrorRequestHandler} */
const onError = (err, req, res, next) => {
if (res.headersSent) return next(err);
res.status(500).json({ error: "Something went wrong" });
};
const app = express();
app.use(express.json());
app.post("/users", createUser);
app.use(onError);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} @import(TypeScript 5.5 and later) bringsRequestHandlerandErrorRequestHandlerinto scope once, instead of repeatingimport("express")in every tag.@typedefnames the shapes.{}as the first type parameter means the route has no parameters.res.json()is now checked againstCreatedUser, so a response with a missing or misspelled field is an error.- Express treats a middleware function with four parameters as an error handler.
ErrorRequestHandlertypes all four;errisany, because anything can be thrown.
A type on req.body tells the checker what you expect, not what a client sent. Check the body at runtime before you trust it; the guide on runtime types and annotations shows how one function can do both.
Checklist
- Install
@types/express(and@types/node) as development dependencies. - Put
@satisfies {import("express").RequestHandler<…>}on each named handler. - Leave inline handlers alone: the route string types their parameters.
- Type the body with the third type parameter, and validate it at runtime.
Under TypeScript 6.0 and 7.0
Every example on this page also runs under TypeScript 6.0.3 and 7.0.2 when this site is built. One of them reports something different from 5.9.3, the version the page describes; the ✗ on its badge links to its entry below.
A function typed with @type is not checked for a missing return
The pattern: /** @type {RequestHandler} */ on an arrow function that returns nothing, where the handler type's return type is not void.
Why: 5.9 reports TS2355 ("must return a value") on the arrow function; TypeScript 7.0.2 does not. The port's list of changes does not mention it. Express ignores what a handler returns, so the code itself is fine. Source: CHANGES.md: changes between TypeScript 6 and the native port.
@type on a handler that returns nothing server.js
- TypeScript 5.9.3
server.js(7,17): error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
- TypeScript 6.0.3same as 5.9.3
The same errors as 5.9.3.
- TypeScript 7.0.2differs from 5.9.3
No errors.
What works in every version: Use @satisfies instead of @type: the handler is checked against the type and keeps its own return type, the same in every version.
The example A named handler and an inline one on this page shows it, with the same result under every version.
Questions
Do I need TypeScript installed to use @types/express?
You need the typescript package as a development dependency to run the checker, but your code stays JavaScript. @types/express is read only by tsc and your editor; nothing in it runs.
Why is req.body typed any?
Express cannot know what a client sends, so @types/express types the body as any until you pass a body type as the third type parameter of RequestHandler. That type is a promise about the data, not a check: validate the body at runtime as well.
Sources
- Routing · Express docs
- Writing error handlers · Express docs
- @types/express · npm
- TypeScript 5.0 release notes (@satisfies in JSDoc) · TypeScript handbook
- TypeScript 5.5 release notes (the @import tag) · TypeScript handbook
Last reviewed 21 October 2026.