Writing code with TypeScript is great, but there are some places in the code where it’s really tricky to let TypeScript shine. One of them is when defining routes in Express.
This article goes through some of the ways to add TypeScript to the Express.Request and Express.Response objects.
Setting up Express
Let’s do the basic stuff first.
import Express from 'express';
import http from 'http';
let app: Express.Application | undefined = undefined;
export function InitializeExpress(port = 3001): void {
app = Express();
app.use(Express.urlencoded({ extended: true }));
app.get('/', function (_req: Express.Request, res: Express.Response) {
res.status(200).json({
"Foo": "Bar",
"Time": new Date().toISOString()
});
});
// Add 404 handler
app.use(function (_req: Express.Request, res: Express.Response) {
res.status(404).send("Not found");
});
// Start server (app.listen can also be used)
http.createServer(app).listen(port, () =>
console.log(`Running at [http://localhost:${port}/`](http://localhost:${port}/`)));
}
On accessing http://localhost:3001/, this server will return something like:
{"Foo":"Bar","Time":"2021-09-21T19:32:15.091Z"}
Express.Request.body
The Express.Request object is defined as:
interface e.Request<P = ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = QueryString.ParsedQs, Locals extends Record<string, any> = Record<string, any>>
and this type is complex to use. To simplify this, a new type is defined as:
export interface TypedRequestBody<T> extends Express.Request {
body: T
}
With this new interface it’s possible to use generics to type the body:
app.post(
"/login",
function (
req: TypedRequestBody<{ username: string, password: string }>,
res: Express.Response
) {
const success = req.body.username === "foo" && req.body.password === "bar";
res.status(200).json({ Success: success });
}
);
This will give the TypeScript checking as well as the intellisense:
Express.Request .query
Modifying the query in Express.Request is somewhat more tricky, because Express uses types from express-serve-static-core , which in turn Express doesn’t expose. So the only way to get hold of the proper types is to import { Query } from 'express-static-serve-core'; . The solution is to do:
import { Query } from 'express-serve-static-core';
export interface TypedRequestQuery<T extends Query> extends Express.Request {
query: T
}
To use it it would look something like:
app.get(
"/article/:id",
function (req: TypedRequestQuery<{ id: string }>, res: Express.Response) {
const id = req.query.id;
// Fetch from database here and return article
res.status(200).json({ ID: id });
}
);
Express.Request .body & query
Let’s do another type where *both *body and query can be typed:
export interface TypedRequest<T extends Query, U> extends Express.Request {
body: U,
query: T
}
The usage could look something like:
app.put(
"/article/:id",
function (
req: TypedRequest<{ id: string }, { name: string }>,
res: Express.Response
) {
console.log(`Updating article ${req.query.id}`);
console.log(`setting name to ${req.body.name}`);
res.status(200).json({ Success: true });
}
);
A small security note on incoming requests
TypeScript doesn’t exist at runtime so even the code is setup to expect a payload of:
{ username: string, password: string }
... bad payloads such as this one:
{ username: undefined, password: ["Hello", "World"] }
.. can be submitted in order to “mess” with the route. Therefore, always make sure to always validate the incoming payload.
PS:
If you want to automatically generate validator functions from TypeScript interfaces / Types, then read this article: https://javascript.plainenglish.io/how-to-automatically-convert-typescript-types-to-runtime-validators-5b06ee269b13
Express.Response
Again Express uses types from express-serve-static-core :
import { Send } from "express-serve-static-core";
The interface is defined as:
export interface TypedResponse<ResBody> extends Express.Response {
json: Send<ResBody, this>;
}
Usage could look something like:
app.get(
"/ping",
function (_req: Express.Request, res: TypedResponse<{ Pong: string }>) {
res.status(200).json({ Pong: new Date().toISOString() });
}
);
And again, if the format of the response doesn’t match the expected result, TypeScript/VSCode (ErrorLens) will inform with an error:
Demo source code?
https://github.com/tomnil/typedexpress
Further Reading
Sharing Types Between Your Frontend and Backend Applications