-
-
Notifications
You must be signed in to change notification settings - Fork 481
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
feat: narrow type based on status code #1970
base: main
Are you sure you want to change the base?
feat: narrow type based on status code #1970
Conversation
🦋 Changeset detectedLatest commit: 7aa73ca The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
I LOVE this! Please let us know if you need any help with the tests or the current direction. This would be amazing to land |
ff0685c
to
c4f66ce
Compare
@drwpow I think this is ready for a review now :) |
7cff619
to
21c7f26
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great! But the FIXME
in the type tests for if (result.data)
vs if (result.error)
could be a breaking change, and could be a type error. The status narrowing seems to work brilliantly, but we’ll need to make sure existing users don’t introduce bugs with this new feature by making sure the current type checks work.
If we can find a way to not have the FIXME
in the type tests, I think we’ve got it.
@@ -158,17 +161,18 @@ The `POST()` request required a `body` object that provided all necessary [reque | |||
|
|||
### Response | |||
|
|||
All methods return an object with **data**, **error**, and **response**. | |||
All methods return an object with **data**, **error**, **status** and **response**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great docs updates! Thank you
@@ -20,22 +20,38 @@ describe("response", () => { | |||
// 2. assert data is not undefined inside condition block | |||
if (result.data) { | |||
assertType<NonNullable<Resource[]>>(result.data); | |||
assertType<undefined>(result.error); | |||
// @ts-expect-error FIXME: This is a limitation within Typescript |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love these tests, and this is a great start! But I fear that we’ll have to fix this before merging (it’ll be much more difficult to fix in a followup, when other work has built off the inference). I don’t really care if the type is undefined
or never
—that’s a detail—but TS should be able to infer the other property otherwise there could be issues!
|
||
if (result.status === 200) { | ||
assertType<NonNullable<Resource[]>>(result.data); | ||
assertType<never>(result.error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are good tests! I would want to see the other common codes tested here, too—201
, and 400
–405
, just for completeness (and if they’re not in the schema, we should add them!)
} | ||
|
||
// @ts-expect-error 204 is not defined in the schema | ||
if (result.status === 204) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a REALLY good test I want to keep—testing what happens when a status code is impossible for a given endpoint! But we should test that 204
works somewhere, even if it’s a different test / different endpoint (doesn’t matter where, as long as it’s somewhere, but soft recommendation to just add 204
here since we have all the other checks, and test for the “missing” case in its own test).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super cool!
I've left some comments mostly regarding http status wildcard precedence.
response: Response; | ||
status: OpenApiStatusToHttpStatus<S, keyof ResponseObjectMap<T>>; | ||
data: S extends OkStatus ? ParseAsResponse<GetResponseContent<ResponseObjectMap<T>, Media, S>, Options> : never; | ||
error: S extends ErrorStatus ? GetResponseContent<ResponseObjectMap<T>, Media, S> : never; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is super nice! I like how it removes "specialness" from data versus error.
IIUC, at this point, the only difference between data and error is parseAs. (totally unrelated to this PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already got a PR semi-ready about that #1986 :)
* 'default' returns every not explicitly defined status code | ||
* '2XX' returns all 2XX status codes | ||
* '4XX' returns all 4XX status codes | ||
* '5XX' returns all 5XX status codes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we remove the explicitly defined status codes from the wildcards? From the OpenAPI Spec (https://swagger.io/specification/#responses-object)
If a response is defined using an explicit code, the explicit code definition takes precedence over the range definition for that code.
So for example, if we have: "2xx" -> A
and "204" -> B
, with different, "204" should resolve to B
and I think currently it would resolve to A | B
.
? Exclude<Exclude<OkStatus | ErrorStatus, string>, AllStatuses> | ||
: Status extends "2XX" | ||
? Exclude<OkStatus, string> | ||
: Status extends "4XX" | "5XX" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might help readability if you split OkStatus
/ ErrorStatus
into numerics / wildcards. For example:
export type ErrorStatus = NumericErrorStatus | WildcardErrorStatus | "default";
type NumericErrorStatus = 500 | ...
type WildcardErrorStatus = '5XX' | '4XX';
export type OpenApiStatusToHttpStatus<Status, AllStatuses> = Status extends number
? Status
: Status extends "default"
? Exclude<NumericOKStatus | NumericErrorStatus, AllStatuses>
: Status extends WildcardOKStatus
? NumericOKStatus
: Status extends WildcardErrorStatus
? NumericErrorStatus
: never
(totally optional)
@@ -280,4 +286,179 @@ describe("types", () => { | |||
assertType<Response>({ error: "default application/json" }); | |||
}); | |||
}); | |||
|
|||
describe("OpenApiStatusToHttpStatus", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is super nice to see unit tests for this helper!
I was wondering: IIUC the second argument (AllStatuses
) comes directly from the response object keys. In this case, it could contain wildcards (e.g. "2XX"
). Would it make sense to add tests for these as well? (I have tried pointing out places below where I think this might affect the tests).
@drwpow self note to maintainers: we should probably set up the infrastructure for openapi-typescript-helpers
to have tests :)
}); | ||
|
||
test("returns 200 likes response", () => { | ||
type Status = OpenApiStatusToHttpStatus<"2XX", 200 | 204 | 206 | 404 | 500 | "default">; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels to me like this test case is "degenerate" at least if I understand the usage in this PR correctly:
I would expect the Status
parameter to always be a subtype of AllStatuses
(which in this case it isn't).
For this test case specifically, I would expect that the explicitly / numerically defined status codes are not part of the end result (precedence, as I pointed out in the doc comment of OpenApiStatusToHttpStatus
).
}); | ||
|
||
test("returns default response", () => { | ||
type Status = OpenApiStatusToHttpStatus<"default", 200 | 204 | 206 | 404 | 500 | "default">; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense add a test case where there also is a wildcard (e.g. "2XX"
) in the second argument?
* '2XX' returns all 2XX status codes | ||
* '4XX' returns all 4XX status codes | ||
* '5XX' returns all 5XX status codes | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: IIUC this implementation has the limitation that status codes unknown to openapi-fetch
have to be explicitly defined in the OpenAPI spec so they can be typed on the client side.
For example, "2XX"
in the OpenAPI spec will not match 209
:
if (status === 209) {
// `data` will be never
}
IMO this is a reasonable limitation / design decision.
Relevant excerpt from RFC 7231
HTTP status codes are extensible. HTTP clients are not required to
understand the meaning of all registered status codes, though such
understanding is obviously desirable. However, a client MUST
understand the class of any status code, as indicated by the first
digit, and treat an unrecognized status code as being equivalent to
the x00 status code of that class, with the exception that a
recipient MUST NOT cache a response with an unrecognized status code.
For example, if an unrecognized status code of 471 is received by a
client, the client can assume that there was something wrong with its
request and treat the response as if it had received a 400 (Bad
Request) status code. The response message will usually contain a
representation that explains the status.
Co-authored-by: Drew Powers <[email protected]>
Changes
The various client methods (
GET
,POST
,PUT
e.t.c.) now have an additionalstatus
property.This property allows users to narrow the type of their
data
/error
based on their documented response status codes.How to Review
How can a reviewer review your changes? What should be kept in mind for this review?
Checklist
docs/
updated (if necessary)pnpm run update:examples
run (only applicable for openapi-typescript)