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

feat(HttpResponse): support explicitly empty response body via null generic type #2118

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 16 additions & 16 deletions src/core/HttpResponse.ts
Expand Up @@ -9,18 +9,16 @@ export interface HttpResponseInit extends ResponseInit {
type?: ResponseType
}

declare const bodyType: unique symbol
const bodyType: unique symbol = Symbol('bodyType')

export interface StrictRequest<BodyType extends DefaultBodyType>
extends Request {
export interface StrictRequest<BodyType extends JsonBodyType> extends Request {
Copy link
Member Author

Choose a reason for hiding this comment

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

Technically, StrictRequest only annotates the JSON body reading method so it cannot be of any other type than the JsonBodyType. As in, it cannot suddenly return a stream.

json(): Promise<BodyType>
}

/**
* Opaque `Response` type that supports strict body type.
*/
export interface StrictResponse<BodyType extends DefaultBodyType>
extends Response {
interface StrictResponse<BodyType extends DefaultBodyType> extends Response {
readonly [bodyType]: BodyType
}

Expand All @@ -35,10 +33,15 @@ export interface StrictResponse<BodyType extends DefaultBodyType>
*
* @see {@link https://mswjs.io/docs/api/http-response `HttpResponse` API reference}
*/
export class HttpResponse extends Response {
constructor(body?: BodyInit | null, init?: HttpResponseInit) {
export class HttpResponse<BodyType extends DefaultBodyType>
Copy link
Member Author

Choose a reason for hiding this comment

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

The trick is this:

  1. implements StrictResponse<T>
  2. All static methods return HttpRespone<T> now, not StrictResponse.
  3. The response resolver type is annotated to return HttpResponse<T> as well.

This makes responses constructed and created via static methods to have the same type validation—on the body argument type level, not the return type level of the response resolver.

extends Response
implements StrictResponse<BodyType>
{
[bodyType]: BodyType = null as any

constructor(body?: NoInfer<BodyType> | null, init?: HttpResponseInit) {
const responseInit = normalizeResponseInit(init)
super(body, responseInit)
super(body as BodyInit, responseInit)
decorateResponse(this, responseInit)
}

Expand All @@ -51,7 +54,7 @@ export class HttpResponse extends Response {
static text<BodyType extends string>(
body?: NoInfer<BodyType> | null,
init?: HttpResponseInit,
): StrictResponse<BodyType> {
): HttpResponse<BodyType> {
const responseInit = normalizeResponseInit(init)

if (!responseInit.headers.has('Content-Type')) {
Expand All @@ -68,7 +71,7 @@ export class HttpResponse extends Response {
)
}

return new HttpResponse(body, responseInit) as StrictResponse<BodyType>
return new HttpResponse(body, responseInit)
}

/**
Expand All @@ -78,9 +81,9 @@ export class HttpResponse extends Response {
* HttpResponse.json({ error: 'Not Authorized' }, { status: 401 })
*/
static json<BodyType extends JsonBodyType>(
body?: NoInfer<BodyType> | null,
body?: NoInfer<BodyType> | null | undefined,
init?: HttpResponseInit,
): StrictResponse<BodyType> {
): HttpResponse<BodyType> {
const responseInit = normalizeResponseInit(init)

if (!responseInit.headers.has('Content-Type')) {
Expand All @@ -100,10 +103,7 @@ export class HttpResponse extends Response {
)
}

return new HttpResponse(
responseText,
responseInit,
) as StrictResponse<BodyType>
return new HttpResponse(responseText as BodyType, responseInit)
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/core/handlers/RequestHandler.ts
Expand Up @@ -3,7 +3,7 @@ import { getCallFrame } from '../utils/internal/getCallFrame'
import { isIterable } from '../utils/internal/isIterable'
import type { ResponseResolutionContext } from '../utils/executeHandlers'
import type { MaybePromise } from '../typeUtils'
import { StrictRequest, StrictResponse } from '..//HttpResponse'
import type { StrictRequest, HttpResponse } from '..//HttpResponse'

export type DefaultRequestMultipartBody = Record<
string,
Expand Down Expand Up @@ -40,7 +40,7 @@ export type ResponseResolverReturnType<
> =
| ([ResponseBodyType] extends [undefined]
? Response
: StrictResponse<ResponseBodyType>)
: HttpResponse<ResponseBodyType>)
| undefined
| void

Expand Down Expand Up @@ -122,7 +122,7 @@ export abstract class RequestHandler<
MaybeAsyncResponseResolverReturnType<any>,
MaybeAsyncResponseResolverReturnType<any>
>
private resolverGeneratorResult?: Response | StrictResponse<any>
private resolverGeneratorResult?: Response | HttpResponse<any>
private options?: HandlerOptions

constructor(args: RequestHandlerArgs<HandlerInfo, HandlerOptions>) {
Expand Down Expand Up @@ -326,7 +326,7 @@ export abstract class RequestHandler<

// Clone the previously stored response from the generator
// so that it could be read again.
return this.resolverGeneratorResult.clone() as StrictResponse<any>
return this.resolverGeneratorResult.clone() as HttpResponse<any>
}

if (!this.resolverGenerator) {
Expand Down
6 changes: 3 additions & 3 deletions src/core/passthrough.ts
@@ -1,4 +1,4 @@
import type { StrictResponse } from './HttpResponse'
import type { HttpResponse } from './HttpResponse'

/**
* Performs the intercepted request as-is.
Expand All @@ -14,12 +14,12 @@ import type { StrictResponse } from './HttpResponse'
*
* @see {@link https://mswjs.io/docs/api/passthrough `passthrough()` API reference}
*/
export function passthrough(): StrictResponse<any> {
export function passthrough(): HttpResponse<any> {
return new Response(null, {
status: 302,
statusText: 'Passthrough',
headers: {
'x-msw-intention': 'passthrough',
},
}) as StrictResponse<any>
}) as HttpResponse<any>
}
48 changes: 48 additions & 0 deletions test/typings/http.test-d.ts
Expand Up @@ -55,6 +55,54 @@ it('returns plain Response withouth explicit response body generic', () => {
})
})

it('returns HttpResponse with URLSearchParams as response body', () => {
http.get('/', () => {
return new HttpResponse(new URLSearchParams())
})
})

it('returns HttpResponse with FormData as response body', () => {
http.get('/', () => {
return new HttpResponse(new FormData())
})
})

it('returns HttpResponse with ReadableStream as response body', () => {
http.get('/', () => {
return new HttpResponse(new ReadableStream())
})
})

it('returns HttpResponse with Blob as response body', () => {
http.get('/', () => {
return new HttpResponse(new Blob(['hello']))
})
})

it('returns HttpResponse with ArrayBuffer as response body', () => {
http.get('/', () => {
return new HttpResponse(new ArrayBuffer(5))
})
})

it('supports null as a response body generic argument', () => {
http.get<never, never, null>('/', () => {
return new HttpResponse()
})
http.get<never, never, null>('/', () => {
return new HttpResponse(
// @ts-expect-error Expected null, got a string.
'hello',
)
})
http.get<never, never, null>('/', () => {
return HttpResponse.json(
// @ts-expect-error Expected null, got an object.
{ id: 1 },
)
})
})

it('supports string as a response body generic argument', () => {
http.get<never, never, string>('/', ({ request }) => {
if (request.headers.has('x-foo')) {
Expand Down