1

disclaimer: I am still a little fuzzy on variance in general...

My situation is as follows:

// index.ts
import express from 'express';
import {Request, Response} from 'express';
const app = express();
app.use(handler);

interface BetterRequest extends Request {
    foo: string;
}

function handler(req: BetterRequest, res: Response) {
    req.foo = 'bar';
}

// tsconfig.json
{
    "compilerOptions": {
        "target": "es6",
        "module": "commonjs",
        "strict": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true
    }
}

The error that I get is

[ts]
Argument of type '(req: BetterRequest, res: Response) => void' is not assignable to parameter of type 'RequestHandlerParams'.
Type '(req: BetterRequest, res: Response) => void' is not assignable to type 'RequestHandler'.
    Types of parameters 'req' and 'req' are incompatible.
    Type 'Request' is not assignable to type 'BetterRequest'.
        Property 'foo' is missing in type 'Request'.

enter image description here

I understand what the error is saying and that I could either turn off those warnings from the tsconfig.json file or by a line comment. But that's not what I am after.

How do I properly write code to handle this situation?

is the following the only way?

// index.ts
function handler(req: Request, res: Response) {
    const req2 = req as BetterRequest;
    req2.foo = 'bar';
}
Eric Liprandi
  • 5,324
  • 2
  • 49
  • 68
  • Duplicate of [Extend Express Request object using Typescript](https://stackoverflow.com/questions/37377731/extend-express-request-object-using-typescript)? – jcalz Oct 25 '18 at 00:49
  • @jcalz IMO no: the other thread was about augmenting `Request` while this one is about using a separate interface. – Matt McCutchen Oct 25 '18 at 14:00
  • I think the problems are the same ("I need to add something to every `Request`") and the solution in the other thread is easier and more type-safe than using a separate interface. When you are treating a function parameter like a writable thing, you want covariance for the writable part, but TypeScript gives you contravariance because it assumes that you are reading from function parameters and not writing to them. By locally merging into the `Request` declaration you sidestep this. Your suggestion of making it an optional property is perfect, though. – jcalz Oct 25 '18 at 14:14
  • @jcalz you are correct, the option described in the other ticket would be a good way to do what I am doing. However my question is about TypeScript in general, not specifically about Express. I just used Express as my example since that's where I am having the problem. – Eric Liprandi Oct 25 '18 at 16:42
  • Locally merging some properties into an existing interface works whether or not it's Express. – jcalz Oct 25 '18 at 17:58
  • Locally adding required properties to an interface when untyped JavaScript code is producing objects that are declared as that interface but don't have the extra properties is very unsound and I wouldn't recommend it. – Matt McCutchen Oct 25 '18 at 18:32
  • 1
    Yes, it would have to be an optional property to be fully sound. A required property where the very first thing you do when any of your code touches the object is to add the property is technically unsound but idiomatic enough that I can't bring myself to discourage it very forcefully. I'd feel hypocritical given the way complex objects tend to be built starting with a type asserted lie (`const x = {} as Foo`) and then adding properties until the assertion become true. YMMV of course. – jcalz Oct 26 '18 at 14:03

2 Answers2

3

An explicit cast as you suggested is probably the best way if you know you are going to immediately set the foo property and you want to be able to use the property from that point on without checking whether it is defined. Another approach is to declare the foo property as optional; then a Request can be implicitly converted to a BetterRequest (so your handler can be implicitly converted to an Express handler), but the type of the foo property will include undefined and you'll have to deal with that every time you use the property.

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
  • the latter part of your answer is what's concerning me. I definitely don't want to have it optional all the way through. For 99% of my code, it's set. As mentioned above, this question is more generic for me. I might repost at a later date with non-expressJS stuff. – Eric Liprandi Oct 25 '18 at 16:46
  • 1
    @EricLiprandi you can probably try the typescript union types like below: function handler(req: BetterRequest & Request, res: Response) { req.foo = 'bar'; } I have used something similar before. Hope it helps :) – Leela Venkatesh K Nov 19 '18 at 18:12
  • or you could even use const newReq = req as BetterRequest; I am not sure if that's good way though – Leela Venkatesh K Nov 19 '18 at 18:15
  • Thanks! Making this property optional seems pretty creative and would not have occurred to me. Although I opted for a different—less safe but perhaps more practical— approach, this answer definitely elucidated something for me about the TS compiler. – Dmitry Minkovsky Jun 19 '20 at 11:16
0

The issue occurs at app.use(handler) because, as the error says:

Type 'Request' is not assignable to type 'BetterRequest'. Property 'foo' is missing in type 'Request'.

The compiler rejects this assignment because handler is saying, "I'm expecting that the caller will give me a 'BetterRequest', but app.use() is saying, "For any handler you give me, I can only guarantee that I'll pass in a 'Request'".

I think the most elegant solution, then, is:

app.use(handler as RequestHander)

This type assertion tells app.use() that handler is a RequestHandler, versus what it is in reality, a "BetterRequestHandler". This is fine as long as handler always safely/correctly references req.foo.

I think this improves on Matt McCutchen's suggestion of making req.foo optional, because you don't have to keep checking its value to please the compiler, even if you know the value is there.


As a concrete example, my use-case was to have a validation middleware before my main handler:

interface ValidatedRequest<T extends object> extends Request {
    validation: T,
}

function registerValidate(
    req: ValidatedRequest<RegisterInput>,
    res: Response,
    next: NextFunction,
) {
    const validation : string | RegisterInput = validate<RegisterInput>(req.body);
    if (typeof validation === 'string') {
        return next(badRequest(validation));
    } else {
        req.validation = validation;
        next();
    }
}

function register(
    req: ValidatedRequest<RegisterInput>,
    res: Response,
    next: NextFunction
) {
    // Use req.validation
}

app.post(
    '/path', 
    registerValidate as RequestHandler,
    register as RequestHandler
);
Dmitry Minkovsky
  • 36,185
  • 26
  • 116
  • 160