If you have ever used Express with Typescript you have surely defined new types for your application. You might have defined several types for your models, or for your function arguments or return types. You definitely would have had to create just a few, especially if you are using tslint to check and lint your code.
I mention tslint just because it will mark all your variables, the ones without an explicit type, as being implicitly typed to any
and of course you will get a nice red underline in your IDE or editor. This will undoubtedly push you to create more custom types.
Anyways, as always I degress…
I believe we have all been in the above mentioned scenario, and that is of course the desired consequence of using typescript. I won’t go over why you should use typescript, but there are a couple of situations which will make you think ‘Why did I decide to use Typescript??? Arghh!!!' and even that is part of the experience .
So lets look at a scenario where typescript will make your life a little more difficult, luckily there is a way to get around this problem and continue to enjoy using typescript.
In nodejs, more specifically javascript, due to the nature of the language one can fluidly create new properties on objects. I know some will say that doing so is really bad, but we have all done it and I am sure we will all continue to do it.
Take a look at the code below.
var car = {
name: 'Aventador',
brand: 'Lamborghini',
};
car.color = 'yellow'; // this adds the 'color' property to the 'car' object
This is all fine and dandy and totally valid javascript, so this would naturally also work in nodejs. Duh…
One very cool aspect of working with Express is the ability to extend the framework to support our needs. This is achieved by creating, and/or using middlewares.
Lets look, for a moment, at the Express documentation for middleware functions:
Middleware functions can perform the following tasks:
- Execute any code.
- Make changes to the request and the response objects.
- End the request-response cycle.
- Call the next middleware function in the stack.
It will happen that within your middleware you will perform some work and you will want to add the result to the request or response object for later. This may look something like this.
var app = express()
var router = express.Router()
function customMiddleware(req, res, next) {
// do some work here
// pass the value along the current request-response cycle
req.customProperty = 'hello world';
next();
}
// sample handler in express
router.get('/user/:id', customMiddleware, function (req, res, next) {
// do some work here
const customValue = req.customProperty; // 'hello world'
res.render('test')
})
// mount the router on the app
app.use('/', router)
This is a pretty standard way of working with Express. So what is the problem?
When using typescript, you will get a compilation error from the typescript compiler if you attempt to access an undeclared property, something like:
error TS2339: Property 'x' does not exist on type 'y'.
So if we take a look at the code above and turn it into valid typescript, you will see that it does not change very much, but you will not be able to compile it because you are now accessing an undeclared property.
import express, { Request, Response } from 'express'
var app = express()
var router = express.Router()
function customMiddleware(req: Request, res: Response, next: any) {
// do some work here
req.customProperty = 'hello world'; // error TS2339
next();
}
// sample handler in express
router.get('/user/:id', customMiddleware, function (req: Request, res: Response) {
// do some work here
const customValue = req.customProperty; // error TS2339
res.render('test')
})
// mount the router on the app
app.use('/', router)
I am sure that like me you will start asking yourself ‘how can I get typescript to know that there is a customProperty
on the Request object?’
Well there is one simple way of doing exactly that… letting the typescript compiler know that Request does indeed contain a customProperty
property.
The correct term is Declaration merging and it means exactly what the name states, here is the official documentation for it.
It simply means that at compilation the typescript compiler will merge separate type declarations into a single definition. Doing so will create an extended type, which will contain the properties of all the declarations together.
Looking at the code above you would extend Express and ensure that the TS compiler knows about our customProperty
. Create a new definition file in your project.
//custom.request.d.ts
declare namespace Express {
export interface Request {
customProperty?: string
}
}
The code above simply states that we are adding customProperty
as an optional string to the Request
interface in the Express
namespace.
Now all we have to do is let the compiler know about the new interface declaration. We can do that by simply adding our custom.request.d.ts file to the tsconfig.json
in the files
section.
{
"compilerOptions": {
"module": "commonjs",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"sourceMap": true
},
"files": [
"custom.request.d.ts"
]
}
This will ensure that the typescript compiler will merge the type declaration found in our custom.request.d.ts
file with the one provided by the Express framework.
This way you will not get any compilation errors because of undeclared properties. This way of extending the type declarations is very powerful and flexible. Furthermore it does not pollute the official declarations of the framework.