types: correct ReadableStream methods, allow Response instance for serve routes under a method (#24872)

This commit is contained in:
Alistair Smith
2025-11-20 02:08:49 -05:00
committed by GitHub
parent 788f03454d
commit b38ba38a18
6 changed files with 109 additions and 31 deletions

View File

@@ -23,16 +23,6 @@ interface BunConsumerConvenienceMethods {
* Consume as JSON
*/
json(): Promise<any>;
/**
* Consume as a FormData instance
*/
formData(): Promise<FormData>;
/**
* Consume as an ArrayBuffer
*/
arrayBuffer(): Promise<ArrayBuffer>;
}
declare module "stream/web" {
@@ -51,6 +41,21 @@ declare module "buffer" {
// slightly different from just "copying in the methods" (the difference is
// related to how type parameters are resolved)
bytes(): Promise<Uint8Array<ArrayBuffer>>;
/**
* Consume the blob as a FormData instance
*/
formData(): Promise<FormData>;
/**
* Consume the blob as an ArrayBuffer
*/
arrayBuffer(): Promise<ArrayBuffer>;
/**
* Returns a readable stream of the blob's contents
*/
stream(): ReadableStream<Uint8Array<ArrayBuffer>>;
}
}

View File

@@ -283,7 +283,8 @@ declare module "bun" {
* return new Response();
* },
* websocket: {
* open(ws) {
* data: {} as {accessToken: string | null},
* message(ws) {
* console.log(ws.data.accessToken);
* }
* }
@@ -486,7 +487,9 @@ declare module "bun" {
}
namespace Serve {
type ExtractRouteParams<T> = T extends `${string}:${infer Param}/${infer Rest}`
type ExtractRouteParams<T> = string extends T
? Record<string, string>
: T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractRouteParams<Rest>
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
@@ -549,14 +552,16 @@ declare module "bun" {
[Path in R]:
| BaseRouteValue
| Handler<BunRequest<Path>, Server<WebSocketData>, Response>
| Partial<Record<HTTPMethod, Handler<BunRequest<Path>, Server<WebSocketData>, Response>>>;
| Partial<Record<HTTPMethod, Handler<BunRequest<Path>, Server<WebSocketData>, Response> | Response>>;
};
type RoutesWithUpgrade<WebSocketData, R extends string> = {
[Path in R]:
| BaseRouteValue
| Handler<BunRequest<Path>, Server<WebSocketData>, Response | undefined | void>
| Partial<Record<HTTPMethod, Handler<BunRequest<Path>, Server<WebSocketData>, Response | undefined | void>>>;
| Partial<
Record<HTTPMethod, Handler<BunRequest<Path>, Server<WebSocketData>, Response | undefined | void> | Response>
>;
};
type FetchOrRoutes<WebSocketData, R extends string> =
@@ -786,7 +791,7 @@ declare module "bun" {
* } satisfies Bun.Serve.Options<{ name: string }>;
* ```
*/
type Options<WebSocketData, R extends string = never> = Bun.__internal.XOR<
type Options<WebSocketData, R extends string = string> = Bun.__internal.XOR<
HostnamePortServeOptions<WebSocketData>,
UnixServeOptions<WebSocketData>
> &
@@ -1276,7 +1281,7 @@ declare module "bun" {
* });
* ```
*/
function serve<WebSocketData = undefined, R extends string = string>(
function serve<WebSocketData = undefined, R extends string = never>(
options: Serve.Options<WebSocketData, R>,
): Server<WebSocketData>;
}

View File

@@ -435,7 +435,7 @@ describe("@types/bun integration test", () => {
code: 2322,
line: "24154.ts:11:3",
message:
"Type 'Blob' is not assignable to type 'import(\"buffer\").Blob'.\nThe types returned by 'stream()' are incompatible between these types.\nType 'ReadableStream<Uint8Array<ArrayBuffer>>' is missing the following properties from type 'ReadableStream<any>': blob, text, bytes, json, and 2 more.",
"Type 'Blob' is not assignable to type 'import(\"buffer\").Blob'.\nThe types returned by 'stream()' are incompatible between these types.\nType 'ReadableStream<Uint8Array<ArrayBuffer>>' is missing the following properties from type 'ReadableStream<any>': blob, text, bytes, json",
},
{
code: 2769,

View File

@@ -335,3 +335,8 @@ new Error("asdf", {
new Error("asdf", {
cause: new Error("asdf"),
});
// @ts-expect-error this interface is defined top level in globals.d.ts so we
// are making sure that .d.ts is a module and that anything top level doesn't
// leak to userland
expectType<BunConsumerConvenienceMethods>();

View File

@@ -20,13 +20,18 @@ export default {
expectType(ws.data).is<{ name: string }>();
},
},
} satisfies Bun.ServeOptions<{ name: string }>;
routes: {
"/": req => {
expectType(req.params).is<Record<string, string>>();
},
},
} satisfies Bun.Serve.Options<{ name: string }>;
function expectInstanceOf<T>(value: unknown, constructor: new (...args: any[]) => T): asserts value is T {
expect(value).toBeInstanceOf(constructor);
}
function test<T = undefined, R extends string = never>(
function test<T = undefined, R extends string = string>(
name: string,
options: Bun.Serve.Options<T, R>,
{
@@ -71,6 +76,11 @@ function test<T = undefined, R extends string = never>(
}
test("basic", {
routes: {
"/123": {
"GET": new Response("Cool/great"),
},
},
fetch(req) {
console.log(req.url); // => http://localhost:3000/
return new Response("Hello World");
@@ -459,15 +469,15 @@ test("very basic fetch with websocket message handler", {
fetch: () => new Response("ok"),
websocket: {
message: ws => {
//
expectType(ws).is<Bun.ServerWebSocket<undefined>>();
},
},
});
test("yet another basic fetch and websocket message handler", {
websocket: {
message: () => {
//
message: ws => {
expectType(ws).is<Bun.ServerWebSocket<undefined>>();
},
},
fetch: (req, server) => {
@@ -481,8 +491,8 @@ test("yet another basic fetch and websocket message handler", {
test("websocket + upgrade on a route path", {
websocket: {
message: () => {
//
message: ws => {
expectType(ws).is<Bun.ServerWebSocket<undefined>>();
},
},
routes: {
@@ -563,7 +573,7 @@ test(
if (Math.random() > 0.5) return undefined;
return new Response();
},
websocket: { message() {} },
websocket: { message: ws => expectType(ws).is<Bun.ServerWebSocket<undefined>>() },
},
{
overrideExpectBehavior: server => {
@@ -816,3 +826,44 @@ test("multiple properties combined", {
return new Response(`Combined server error: ${error.message}`, { status: 500 });
},
});
test("#24819 regression", {
development: !process.env.production,
routes: {
"/health": {
GET: new Response("OK"),
POST: req => {
expectType(req).is<Bun.BunRequest<"/health">>();
return Response.json("Sup");
},
},
},
});
// @ts-expect-error
test("#24819 regression with no response requires websocket", {
development: !process.env.production,
routes: {
"/health": {
GET: new Response("OK"),
POST: req => {
expectType(req).is<Bun.BunRequest<"/health">>();
},
},
},
});
test("#24819 regression with websocket is happy", {
websocket: {
message: console.log,
},
development: !process.env.production,
routes: {
"/health": {
GET: new Response("OK"),
POST: req => {
expectType(req).is<Bun.BunRequest<"/health">>();
},
},
},
});

View File

@@ -69,3 +69,15 @@ Bun.file("./foo.csv")
},
}),
);
// @ts-expect-error These properties do not exist right now
expectType(new ReadableStream().arrayBuffer());
// @ts-expect-error These properties do not exist right now
expectType(new ReadableStream().formData());
expectType(new Blob([]).text()).is<Promise<string>>();
expectType(new Blob([]).arrayBuffer()).is<Promise<ArrayBuffer>>();
expectType(new Blob([]).bytes()).is<Promise<Uint8Array<ArrayBuffer>>>();
expectType(new Blob([]).json()).is<Promise<any>>();
expectType(new Blob([]).formData()).is<Promise<FormData>>();
expectType(new Blob([]).stream()).is<ReadableStream<Uint8Array<ArrayBuffer>>>();