Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@type/express-serve-static-core] Allow express RequestHandler to return Response #70696

Conversation

xfournet
Copy link
Contributor

@xfournet xfournet commented Sep 26, 2024

In v4 we used to write handler with a compact syntax like this

app.get("/response", (_, res) => res.json('ok'));

This was possible because;

  • from javascript perspective, express don't care about the returned value of the handler, so returning something doesn't matter
  • from typescript perspective, due to the specific handling of void returning function, it's possible to return anything in a void function.

However, as noted in #70563 using void | Promise<void> cannot accept anything, including Response or Promise<Response>

The PR change the return type of RequestHandler to permit to return Response in addition to void (and the same for async/Promise)


Please fill in this template.

@typescript-bot
Copy link
Contributor

typescript-bot commented Sep 26, 2024

@xfournet Thank you for submitting this PR!

This is a live comment that I will keep updated.

1 package in this PR

Code Reviews

Because this is a widely-used package, a DT maintainer will need to review it before it can be merged.

You can test the changes of this PR in the Playground.

Status

  • ❌ No merge conflicts
  • ✅ Continuous integration tests have passed
  • 🕐 Most recent commit is approved by a DT maintainer

Once every item on this list is checked, I'll ask you for permission to merge and publish the changes.

Inactive

This PR has been inactive for 18 days.


Diagnostic Information: What the bot saw about this PR
{
  "type": "info",
  "now": "-",
  "pr_number": 70696,
  "author": "xfournet",
  "headCommitOid": "f0980de79d0d937d26065f24a1c36510553b595c",
  "mergeBaseOid": "08f56a2bd0d71d12f468b93862aa555c7ec683bb",
  "lastPushDate": "2024-09-26T23:27:27.000Z",
  "lastActivityDate": "2025-01-28T00:47:05.000Z",
  "hasMergeConflict": true,
  "isFirstContribution": false,
  "tooManyFiles": false,
  "hugeChange": false,
  "popularityLevel": "Critical",
  "pkgInfo": [
    {
      "name": "express-serve-static-core",
      "kind": "edit",
      "files": [
        {
          "path": "types/express-serve-static-core/express-serve-static-core-tests.ts",
          "kind": "test"
        },
        {
          "path": "types/express-serve-static-core/index.d.ts",
          "kind": "definition"
        }
      ],
      "owners": [
        "borisyankov",
        "micksatana",
        "JoseLion",
        "dwrss",
        "andoshin11"
      ],
      "addedOwners": [],
      "deletedOwners": [],
      "popularityLevel": "Critical"
    }
  ],
  "reviews": [
    {
      "type": "stale",
      "reviewer": "jakebailey",
      "date": "2024-10-29T19:21:51.000Z",
      "abbrOid": "b3402ab"
    },
    {
      "type": "stale",
      "reviewer": "RobinTail",
      "date": "2024-10-29T11:19:31.000Z",
      "abbrOid": "b3402ab"
    }
  ],
  "mainBotCommentID": 2378114426,
  "ciResult": "pass"
}

@typescript-bot
Copy link
Contributor

🔔 @borisyankov @micksatana @JoseLion @dwrss @andoshin11 — please review this PR in the next few days. Be sure to explicitly select Approve or Request Changes in the GitHub UI so I know what's going on.

@typescript-bot typescript-bot added the The CI failed When GH Actions fails label Sep 26, 2024
@typescript-bot
Copy link
Contributor

typescript-bot commented Sep 26, 2024

@xfournet The CI build failed! Please review the logs for more information.

Once you've pushed the fixes, the build will automatically re-run. Thanks!

Note: builds that are failing do not end up on the list of PRs for the DT maintainers to review.

@typescript-bot typescript-bot added Possibly Edits Infrastructure Too many files, bot didn't see them all Author is Owner The author of this PR is a listed owner of the package. Too Many Files Not all files scanned by the bot! and removed Critical package The CI failed When GH Actions fails labels Sep 27, 2024
@typescript-bot
Copy link
Contributor

🔔 @xfournet — you're the only owner, but it would still be good if you find someone to review this PR in the next few days, otherwise a maintainer will look at it. (And if you do find someone, maybe even recruit them to be a second owner to make future changes easier...)

@typescript-bot typescript-bot added the Self Merge This PR can now be self-merged by the PR author or an owner label Sep 27, 2024
@xfournet xfournet force-pushed the express-handler-return-response branch from 144b97c to fb271d0 Compare September 27, 2024 00:14
@typescript-bot typescript-bot added Critical package The CI failed When GH Actions fails and removed Self Merge This PR can now be self-merged by the PR author or an owner Author is Owner The author of this PR is a listed owner of the package. Possibly Edits Infrastructure Too many files, bot didn't see them all Too Many Files Not all files scanned by the bot! labels Sep 27, 2024
@xfournet xfournet force-pushed the express-handler-return-response branch from fb271d0 to 01d4b30 Compare September 27, 2024 00:36
@typescript-bot typescript-bot removed the The CI failed When GH Actions fails label Sep 27, 2024
@xfournet xfournet force-pushed the express-handler-return-response branch from 01d4b30 to d3691b5 Compare September 27, 2024 08:56
@jakebailey
Copy link
Member

At this point, why even bother typing anything for the result? Why not just write unknown and let it be whatever?

Theoretically this is fine, I suppose, but just kinda weird.

@RobinTail

@RobinTail
Copy link
Contributor

RobinTail commented Sep 27, 2024

from javascript perspective, express don't care about the returned value of the handler, so returning something doesn't matter

if it does not matter, not taken into account and not processed then why are you trying to return something?
Moreover, the suggestion to return something meaningful would lead to confusion, implying that it would affect something, lead to a certain consequence, but it's not.
I see no reason/grounds for such a change, @xfournet

CC @jakebailey

@RobinTail
Copy link
Contributor

RobinTail commented Sep 27, 2024

compact syntax like this

if the compactness is your aim, you can still do it this way, @xfournet :

app.get("/response", (_, res) => void res.json('ok'));

But the strive for compactness does not justify incorrectness.

@jakebailey
Copy link
Member

Or:

app.get("/response", (_, res) => { res.json('ok') });

@RobinTail
Copy link
Contributor

RobinTail commented Sep 27, 2024

Yes, but some code formatters, like Prettier, would make that statement multiline, so it might not be so desirably compact.
So, I kinda understand the concern, but its essence does not seem to me DT related.

@typescript-bot typescript-bot added the Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. label Oct 8, 2024
@typescript-bot
Copy link
Contributor

Re-ping @borisyankov, @micksatana, @JoseLion, @dwrss, @andoshin11:

This PR has been out for over a week, yet I haven't seen any reviews.

Could someone please give it some attention? Thanks!

@typescript-bot typescript-bot added Revision needed This PR needs code changes before it can be merged. and removed Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. labels Oct 23, 2024
@typescript-bot
Copy link
Contributor

@xfournet One or more reviewers has requested changes. Please address their comments. I'll be back once they sign off or you've pushed new commits. Thank you!

@xfournet
Copy link
Contributor Author

Thanks all for your feedbacks, i run out of time and i'm a bit late to answer, sorry for that

@RobinTail

if it does not matter, not taken into account and not processed then why are you trying to return something?

my point is about backward compatibility with existing codebase for which Typescript is now failing while the code is still executing correctly from a javascript point of view. From the javascript perspective Express 5 is properly testing if the returned value is a promise, and only process the value in this case.
I simplified a bit my example, but like @Mauro-Domingues i see a lot a control-flow based on returning the response in our codebase.
The fact is that typescript void return is unrestrictive and this has lead some codebase to adopt such patterns given the v4 type definition. Should we really break them ? I known it's a major version, but no deprecation path like for other Express changes could be a bit abrupt and prevent some migration to Express 5.

Moreover, the suggestion to return something meaningful would lead to confusion, implying that it would affect something, lead to a certain consequence, but it's not.

Yes you're right i'm changing it to unknown | Promise<unknown>

@jakebailey

At this point, why even bother typing anything for the result? Why not just write unknown and let it be whatever?

Yes, i updated the PR with unknown | Promise<unknown> to be less confusing. Note that we can't just use unknown in case people (like me) using eslint @typescript-eslint/no-misused-promises rule (error: Promise returned in function argument where a void return was expected)

@xfournet xfournet force-pushed the express-handler-return-response branch from d3691b5 to e7e16f5 Compare October 29, 2024 10:38
@typescript-bot typescript-bot added Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. and removed Revision needed This PR needs code changes before it can be merged. labels Oct 29, 2024
@xfournet xfournet force-pushed the express-handler-return-response branch from e7e16f5 to b3402ab Compare October 29, 2024 10:40
@typescript-bot
Copy link
Contributor

@jakebailey Thank you for reviewing this PR! The author has pushed new commits since your last review. Could you take another look and submit a fresh review?

RobinTail

This comment was marked as outdated.

@@ -61,7 +61,7 @@ export interface RequestHandler<
req: Request<P, ResBody, ReqBody, ReqQuery, LocalsObj>,
res: Response<ResBody, LocalsObj>,
next: NextFunction,
): void | Promise<void>;
): unknown | Promise<unknown>;
Copy link
Member

@jakebailey jakebailey Oct 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why the tests aren't failing with this. This is equivalent to writing "unknown" because the union is reduced. I don't think this is a good type to use.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my previous comment

Note that we can't just use unknown in case people (like me) using eslint @typescript-eslint/no-misused-promises rule (error: Promise returned in function argument where a void return was expected)

From a strictly TypeScript perspective, you're correct; we could even have kept void as in v4 since void doesn’t limit the return type, allowing Promise to be returned with void or unknown alone. However, this approach can be cumbersome for those using @typescript-eslint/no-misused-promises, which requires the return type to explicitly reference Promise.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, but that sure seems like a linter bug. unknown | number is unknown. The types are eaten.
image

I'm not sure how they can detect this unless they are somehow reading out the AST for the originally declared type, which will only ever work for hand-written code. If you wrote this code in TS today, it's absolutely free to not reprint this type as a union.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how they can detect this unless they are somehow reading out the AST for the originally declared type, which will only ever work for hand-written code. If you wrote this code in TS today, it's absolutely free to not reprint this type as a union.

you're right, i double checked my test, and i should have done something wrong previously. Returning a promise on a function declared has unknown is not reported from @typescript-eslint/no-misused-promises.
I'm changing the PR

@typescript-bot typescript-bot added Revision needed This PR needs code changes before it can be merged. and removed Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. labels Oct 29, 2024
@typescript-bot
Copy link
Contributor

@xfournet One or more reviewers has requested changes. Please address their comments. I'll be back once they sign off or you've pushed new commits. Thank you!

@xfournet xfournet force-pushed the express-handler-return-response branch from b3402ab to f0980de Compare October 30, 2024 10:46
@typescript-bot typescript-bot added Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. and removed Revision needed This PR needs code changes before it can be merged. labels Oct 30, 2024
@typescript-bot
Copy link
Contributor

@jakebailey, @RobinTail Thank you for reviewing this PR! The author has pushed new commits since your last review. Could you take another look and submit a fresh review?

@jakebailey
Copy link
Member

Code wise, the PR is fine, but I think I'd like to see some sort of consensus that this should change.

@RobinTail
Copy link
Contributor

I think we're facing a conceptual problem here: should DT describe the types strictly to ensure constraints that help to avoid mistakes, OR should those types be loose enough to fit:

  • imperfections,
  • desire for neatness,
  • unwillingness to add curly braces,
  • particular ESLint rules.

I've been personally leaning to the first opinion, but I understand that DT used by millions of people with different opinions on that matter. So, I'm going to refrain and rely on opinion of DT maintainers, @jakebailey

@andrewbranch
Copy link
Member

DT types are supposed to describe the public contract of the package, without being overly burdensome. For example, I recently discouraged a contributor from typing each color component of an RGB input option as a union of all integers between 0 and 255—that’s correctness that imposes too great a burden on API consumers.

I personally don’t love this change, as I think void has some marginal documentation value. But this is a decision we’d like the types maintainers to make.

@Mauro-Domingues
Copy link

Mauro-Domingues commented Nov 5, 2024

Thanks all for your feedbacks, i run out of time and i'm a bit late to answer, sorry for that

@RobinTail

if it does not matter, not taken into account and not processed then why are you trying to return something?

my point is about backward compatibility with existing codebase for which Typescript is now failing while the code is still executing correctly from a javascript point of view. From the javascript perspective Express 5 is properly testing if the returned value is a promise, and only process the value in this case. I simplified a bit my example, but like @Mauro-Domingues i see a lot a control-flow based on returning the response in our codebase. The fact is that typescript void return is unrestrictive and this has lead some codebase to adopt such patterns given the v4 type definition. Should we really break them ? I known it's a major version, but no deprecation path like for other Express changes could be a bit abrupt and prevent some migration to Express 5.

Moreover, the suggestion to return something meaningful would lead to confusion, implying that it would affect something, lead to a certain consequence, but it's not.

Yes you're right i'm changing it to unknown | Promise<unknown>

@jakebailey

At this point, why even bother typing anything for the result? Why not just write unknown and let it be whatever?

Yes, i updated the PR with unknown | Promise<unknown> to be less confusing. Note that we can't just use unknown in case people (like me) using eslint @typescript-eslint/no-misused-promises rule (error: Promise returned in function argument where a void return was expected)

I have the habit of having explicit return on my functions because it is a good practice and helps other developers to understand the code. In fact, removing the "return" and leaving it as Promise<void> solves the problem, but some factors can influence this change such as documentation generators and human errors because Promise<void> is not restrictive. But I am consented to the change.

@jakebailey
Copy link
Member

Given the lack of any further requests for this (here, in issues, or discussions), I feel like this can be closed.

@typescript-bot typescript-bot added Has Merge Conflict This PR can't be merged because it has a merge conflict. The author needs to update it. and removed Unreviewed No one showed up to review this PR, so it'll be reviewed by a DT maintainer. labels Jan 28, 2025
@typescript-bot
Copy link
Contributor

@xfournet Unfortunately, this pull request currently has a merge conflict 😥. Please update your PR branch to be up-to-date with respect to master. Have a nice day!

@xfournet xfournet closed this Feb 15, 2025
@KalenAnson
Copy link

KalenAnson commented Feb 23, 2025

I just got bit with this issue (re: expressjs/express#5987).

I would argue that allowing express to return from a route hander only improves the readability of code and promotes secure operation of the handler.

Most handlers are not quite so simple as this:

app.get("/response", (_, res) => res.json('ok'));

Often there is loads of validation (often offloaded to middleware) and business logic in the route handler.

Letting the route handler call return res.send() helps developers express in simple terms that the route handler is now done™. I have used this paradigm since express 1.0 to avoid shooting myself in the foot. Pulling this very intuitive backstop makes it very easy to have the handler do bad things non-intuitively.

Love to see this decision reconsidered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Critical package Has Merge Conflict This PR can't be merged because it has a merge conflict. The author needs to update it.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants