Skip to content

bun-types: take a URL where the runtime does, let a class extend ReadableStream again, declare WebAssembly.Tag, Exception and JSTag - #48359

Open
robobun wants to merge 5 commits into
mainfrom
robobun/045e90c8/bun-types-web-globals-follow-runtime
Open

robobun wants to merge 5 commits into
mainfrom
robobun/045e90c8/bun-types-web-globals-follow-runtime

Conversation

@robobun

@robobun robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A bun init project does not compile code that runs: new Request(new URL(..)) (TS2769), class S extends ReadableStream<string> {} (TS2344: Type 'string' does not satisfy the constraint 'Uint8Array<ArrayBufferLike>'), WebAssembly.Tag (TS2339). bun build --compile --aot stops there.
  • Cause: the declarations without lib.dom in packages/bun-types. The ReadableStream case regressed in Fix every type error in test/ #44614 (bun-types 1.4.3).

Fix

  • new Request(url), new Request({ url }), Response.redirect, URL.canParse and URL.parse take a URL. WebAssembly.Tag, Exception and JSTag are declared.
  • The byte-source signature of ReadableStream has no constraint on the chunk type, so both extends forms compile. Without a type argument, a call infers the chunk type as on main.
  • Self-reviewed: 70 concerns raised, 44 addressed. The other 26 are effects of the fix itself (Notes).
  • Verified: bun-types.test.ts, the new test/cli/check/web-globals-take-what-the-runtime-takes.test.ts, and test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts.

Background

  • bun init sets "lib": ["ESNext"], so bun-types declares the web globals itself. With lib.dom, lib.dom's declarations apply.
  • An extends clause checks its type arguments against each construct signature. The constraint R extends Uint8Array refused string there.
  • The new type parameter K is inferred only by a call, from type: "bytes". With it the chunk type has main's constraint and default.

Downsides

  • A type argument on a byte source is no longer checked: new ReadableStream<string>({ type: "bytes" }) compiles, as with lib.dom and @types/node. 1.4.3 and 1.4.4 refuse it.
  • Code that reads a widened parameter sees string | URL, for example a mock of URL.canParse.
  • Size: +3,925 bytes of .d.ts, +342 in the binary. tsc allocations: +45 per byte stream, +17 per extends ReadableStream<T>.
Notes

What takes a URL now, without lib.dom

  • new Request(url, init?) and the url member of new Request({ url, ... }).
  • Response.redirect(url, status) and Response.redirect(url, init).
  • Both arguments of URL.canParse(url, base?) and URL.parse(url, base?).
  • The runtime takes each of them. The program of the new test runs all eight positions on Bun before bun check reads it.

The byte-source signature of ReadableStream, without lib.dom

  • ReadableStream keeps three construct signatures, in main's order: default source, direct source, byte source. Only the third changes.
  • Main: new <R extends Uint8Array = Uint8Array<ArrayBuffer>>(source: UnderlyingByteSource, strategy?): ReadableStream<R>. An extends clause reads every signature that takes its number of type arguments. For extends ReadableStream<string> the constraint is TS2344. For extends ReadableStream the default gives a return type that the other two signatures do not have (TS2510).
  • Now the signature has three type parameters: R = any, K extends "bytes" = never, and B, and it returns ReadableStream<B>. A call without type arguments infers K as "bytes" from the type member of its source. Then B has the constraint Uint8Array and the default Uint8Array<ArrayBuffer>, as R has on main, and an expected type sets it.
  • An extends clause and a call with a type argument infer nothing, so K keeps its default never. Then B is R without a constraint, and the signature returns ReadableStream<R> as the other two do.
  • The parameter is a conditional type on K that is UnderlyingByteSource for each K. Its other branch only gives a call the place to infer K from. So ConstructorParameters<typeof ReadableStream>, a subclass, and a union of the class with a subclass see main's parameter type.
  • Kept from Fix every type error in test/ #44614 (Fix every type error in test/ #44614 (comment)): a source of type any gives ReadableStream<any>, and new ReadableStream<Uint8Array>({ type: "bytes" }) compiles. The test "web globals without lib.dom > follow the runtime" now pins the first.

What differs from main for new ReadableStream, without lib.dom

  • Accepted now, refused on main: a byte source with a type argument that is not a Uint8Array. Examples: <string>, <string | Uint8Array>, <Uint8Array | undefined>, a type parameter without a constraint, and super({ type: "bytes" }) in a class that extends ReadableStream<string>. The program states the chunk type there, and the declarations do not check it against the source. lib.dom (TypeScript 5.7 to 7.1) and @types/node 26 accept all of these.
  • The same calls, when something else in them is wrong: main did not try the byte-source signature for <string>, and now it is the last one tried. So the error of such a rejected call can move to another line, and a // @ts-expect-error on the old line no longer takes it.
  • As on main: new ReadableStream<T>({ type: "bytes" }) and super({ type: "bytes" }) with T extends Uint8Array, new ReadableStream<never>({ type: "bytes" }), and const s: ReadableStream<string> = new ReadableStream({ type: "bytes" }) is an error.
  • As on main: an expected type sets the chunk type. Of 30 forms with a union in the expected type, main and this PR refuse the same 2 on TypeScript 6 and 7 and the same 18 on TypeScript 5.7 to 5.9.
  • As on main: const s: ReadableStream<Buffer> = new ReadableStream({ type: "bytes" }) compiles. At run time a chunk of a byte stream is a plain Uint8Array, so the tests do not pin this form.
  • As on main: InstanceType<typeof ReadableStream> is ReadableStream<Uint8Array>, ConstructorParameters<typeof ReadableStream>[0] is UnderlyingByteSource, a type that reads three construct signatures gets the same three sources, and new (flag ? Sub : ReadableStream)(source) compiles.
  • New: a class that extends ReadableStream without a type argument takes a byte source in new and in super().
  • Checked JavaScript: of 20 subclass forms, 12 fail on main on TypeScript 5.7 to 7.0 (the bare form, @extends {ReadableStream<string>} and others). With this PR none fails.
  • As on main: the error report of a rejected call without a type argument (1,191 generated lines on tsc 5.9.3, 6.0.3 and 7.0.2, with the message text). TypeScript 5.7 to 6.0 lists the three signatures, and TypeScript 7 reports through the last one, the byte source.
  • Not as on main: new on the declared union typeof ReadableStream | typeof Sub, where Sub<T extends Uint8Array = Uint8Array<ArrayBuffer>> is a generic subclass with main's constraint and default, is TS2351. Main's third signature was identical to the one that Sub inherits, and the new one is not.

Earlier shapes of the byte-source signature in this PR

  • First commit: a signature with a fixed ReadableStream<Uint8Array<ArrayBuffer>> return type, hidden from an extends clause by two more type parameters, and a second signature for a type argument. An expected type no longer set the chunk type.
  • Second commit (the pushed head before this one): the hidden signature returned ReadableStream<R | A | B>, and the second one had the parameter R extends Uint8Array ? UnderlyingByteSource : never. TypeScript does not resolve that condition for a type parameter, so new ReadableStream<T>(source) with T extends Uint8Array did not compile. TypeScript 6 and 7 did not infer R through the union in the return type (16 of 30 forms that main compiles failed). Checked JavaScript on TypeScript 5.7 to 6.0 lost @extends {ReadableStream<Uint8Array>}.
  • Every shape with two byte-source signatures has four signatures in all. Then TypeScript 5.7 to 6.0 reports a rejected call through the last signature only (it lists up to three), InstanceType<typeof ReadableStream> is ReadableStream<unknown>, and a type that reads three signatures loses the default source. One signature avoids all three. It costs +45 tsc allocations per byte stream, against +4 for the cheapest pair.
  • Measured and not used: one signature with the parameter UnderlyingByteSource & { type: .. K .. }. TypeScript puts any for the type parameters when it compares two constructor types, and that parameter then has any in it. So new (flag ? Sub : ReadableStream)(source) was TS2351.
  • Measured and not used: a parameter that keeps main's refusal of <string> through a lookup type ({ BYTES_PER_ELEMENT: "bytes" }[keyof R & "BYTES_PER_ELEMENT"]). It accepts every typed array, and it rests on how TypeScript assigns to an indexed access of a type parameter.

WebAssembly

  • Without lib.dom, WebAssembly.Tag has the member readonly [Symbol.toStringTag]: "WebAssembly.Tag", which the prototype has at run time. So a number or a plain object is no tag. ExportValue includes Tag, so { env: { tag: WebAssembly.JSTag } } is an import object. A string is still refused as an import.
  • With lib.dom loaded, Tag stays an empty interface as in lib.dom, and ExportValue has no tag. The second commit put the member on lib.dom's Tag too, and instance.exports.x as WebAssembly.Tag became TS2352 there. That is gone, and the fixture pins the cast and the refused string import in both configurations.
  • With lib.dom on TypeScript 6.0 and later, no line of the probe set that compiles on main fails. A tag import through WebAssembly.instantiate stays TS2769 there, as with lib.dom alone.
  • TypeScript 5.7 to 5.9 have no Tag, Exception or JSTag in lib.dom. They get them from bun-types with lib.dom loaded too, with the empty Tag.
  • Memory.toFixedLengthBuffer and toResizableBuffer were in the first two commits and are out. As required members they break a hand-written WebAssembly.Memory and feature detection with in, and their return type for a shared memory needs a decision. A separate change carries them.
  • ExceptionOptions.traceStack is boolean | undefined, for exactOptionalPropertyTypes.
  • With "lib": ["ESNext", "WebWorker"] and no DOM, bun-types applies its declarations for a project without lib.dom, as for every other global. lib.webworker's empty Tag then gets the member, and a cast of an export to WebAssembly.Tag is TS2352.

Effects of the fix on code that compiles on main (each comes with the declaration itself)

  • A parameter that now takes string | URL: a callback without a parameter type for spyOn(URL, "canParse").mockImplementation(url => url.startsWith(..)), URL.canParse passed to a generic function that then infers string | URL where it inferred string, a static override with url: string | null, and a class assigned to typeof Request whose constructor takes { url: string }. lib.dom and @types/node declare canParse and parse with string | URL too.
  • ConstructorParameters<typeof Request>[0] is string | URL | Request (main: Request), and Reflect.construct(Request, [url, init]) is Request (main: any). Of 41 sites that read such derived types and compile on main, 1 is an error now: ConstructorParameters<typeof Request>[0].url.
  • New names: a // @ts-expect-error on a line that now compiles is TS2578. A project that declares WebAssembly.Tag, Exception or JSTag itself gets TS2300, TS2451 or TS2403. After if (!("Tag" in wasm)) on an alias of the namespace the value is never. Record<keyof typeof WebAssembly, ..> needs three more keys.
  • Without lib.dom an export can be a Tag. So const e: Record<string, Function | WebAssembly.Global | WebAssembly.Memory | WebAssembly.Table> = instance.exports is TS2322, and code that narrows an export by exclusion sees one more member.
  • A project that repeats the declaration of the global URL, Request or Response with main's type gets TS2403.
  • Contrived forms that also differ: a source whose type is never, a constructor type that adds a signature after those of ReadableStream, new on a union of typeof ReadableStream with an unrelated constructor type (TypeScript 5.7 to 6.0 then give ReadableStream<any> for a byte source), Symbol.toStringTag in value on an export, and an object of type URL that is no URL instance.

Self-review

  • Before the first push: 11 concerns. The first draft also accepted a TextDecoder label of type string and a null listener of addEventListener. Both are decisions for the code owner, so they are out. TextDecoder label case is bun-types: accept a TextDecoder label in any ASCII case #48360. URL.canParse and URL.parse came in.
  • Before the third commit: 8 concerns. The cast to WebAssembly.Tag with lib.dom, the union expected type, the checked JavaScript forms, and pins that said a byte stream yields Buffer.
  • On the third commit: a search for programs that compile on main and fail with this PR gave 51 findings. 25 are fixed: the single signature (9), the empty Tag interface with lib.dom (1), the Memory methods taken out (7), new pins (3), sentences of this description (5). The other 26 come with the fix itself. The sections above list their kinds.
  • Kept: the 60-second limit of the bun check test. One bun check reads every declaration of bun-types and @types/node. On a debug build the test takes 4 s to 14 s locally, and the default limit is 5 s.

Still rejected, on purpose or owned elsewhere

Reach

  • The changed declarations apply to a project whose tsconfig has no DOM library. bun init writes "lib": ["ESNext"] for a blank project and "lib": ["ESNext", "DOM"] for its three React templates.
  • A build without a tsconfig and without installed types reads TypeScript's default library and the declarations that Bun brings, and Bun's ReadableStream constructor applies there. So class Pipe extends ReadableStream {} builds now, where main stops with TS2510. test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts pinned that stop (AOT: the next base #48120). The class moved to the programs that build and run.
  • The fix reaches a project with the next bun-types release. --aot (Compile TypeScript to machine code ahead of time: bun build --compile --aot #45631) is in no stable release yet.

Measurements ("six compilers" are tsc 5.7.3, 5.8.3, 5.9.3, 6.0.3, 7.0.2 and 7.1.0-beta, other numbers are from tsc 7.0.2)

  • The program of the new test: 13 diagnostics on main's declarations, 0 with this PR. It prints the same on Bun 1.4.3 and on the debug build of this branch. bun check on it exits 1 on main's declarations and 0 with this PR. bun build --compile --aot of a program with these forms exits 0, and the executable prints what bun index.ts prints.
  • test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts: 60 pass with this PR, and its new case stops with TS2510 on main's declarations. test/aot/missing-bun-types.test.ts: 91 of 92 pass. The one failure is a build that exits 1 without a diagnostic on the debug build, in a test that passed in the runs before.
  • bun-types.test.ts: its 31 type tests pass with this PR. 12 of them fail on main's declarations, and the same 12 on the declarations of the second commit. The bindgen test of that file does not run here since the merge of main: it stops in src/runtime/api/bun/subprocess.bind.ts under the installed Bun 1.4.3, with or without these declarations. Fixture without lib.dom: 28 diagnostics on main, 0 with this PR. With lib.dom: the 40 pinned ones.
  • Probe set: 140 TypeScript and 20 JavaScript files (mine and the reviews'), with and without lib.dom, on six compilers. Also two generated sets of the reviews on tsc 5.9.3, 6.0.3 and 7.0.2, compared with the message text: 7,766 lines of calls with each kind of source against 52 expected types, and 1,191 lines of rejected calls without a type argument. No diagnostic differs from main. Lines that compile on main and fail with this PR: with lib.dom on TypeScript 6.0 and later 0, in TypeScript and in checked JavaScript. Without lib.dom every such line is of a kind that this body names.
  • bun check of the debug build reports the same lines as tsc 7.0.2 on that set, in all four configurations.
  • Run time: 0 files of compiled code changed.
  • Check time: the Memory allocs line of tsc --extendedDiagnostics --singleThreaded, per extra call site (slope between 1,001 and 2,001 sites, median of 9 runs). new ReadableStream({ type: "bytes", pull(c) { c.close(); } }): 105.6 on main, 150.6 with this PR. class X extends ReadableStream<Uint8Array> {}: 71.5 and 88.5. A default stream: 70.3 and 70.4. A direct stream: 90.6 and 90.6. 16 other forms differ by 0.02 at most.
  • Instantiations, first use: new Request("…") 2 and 2. new Request(new URL(..)): 274 (an error on main) and 67. A byte stream: 275 and 289.
  • Empty program: symbols +49, types +0, instantiations +0, allocations +57 of 116,059 (the 15 runs of one tree differ by up to 63).
  • Bytes: globals.d.ts +487, wasm.d.ts +3,438. The packed copy in the binary (comments removed, zstd) grows by 342 bytes.
  • The repository's own test project (tsc -p test/tsconfig.json): 1,441 diagnostics on main and with this PR, 0 new and 0 gone.
  • The typecheck jobs of lint.yml (build scripts, built-in modules, src, the dev server): 0 errors.

…ableStream again, declare WebAssembly.Tag, Exception and JSTag

Without lib.dom, bun-types declares the web globals itself. Three of
those declarations were narrower than the runtime, so a program that
runs on Bun was a type error in the project that `bun init` writes.

- `new Request(new URL(...))`, `new Request({ url: new URL(...) })`,
  `Response.redirect(new URL(...))`, `URL.canParse()` and `URL.parse()`
  take a URL object, as the runtime does.
- `class S extends ReadableStream<string> {}` and
  `class S extends ReadableStream {}` compile again. Both compiled
  through bun-types 1.4.2. The byte-source construct signature that
  #44614 added took part in the `extends` clause. It is now two
  signatures that an `extends` clause can read.
- `WebAssembly.Tag`, `WebAssembly.Exception`, `WebAssembly.JSTag` and the
  `toResizableBuffer` and `toFixedLengthBuffer` methods of
  `WebAssembly.Memory` are declared, with the types of lib.dom.

With lib.dom loaded the types stay as they are. TypeScript 5.7 to 5.9
now gets the WebAssembly members that its lib.dom does not have.
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 49 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 89452d2 and 1f04f9b.

Files selected for processing: 8

  • packages/bun-types/globals.d.ts
  • packages/bun-types/wasm.d.ts
  • test/aot/what-rests-on-a-brought-declaration-stops-no-build.test.ts
  • test/cli/check/web-globals-take-what-the-runtime-takes.test.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/fetch.ts
  • test/integration/bun-types/fixture/streams.ts
  • test/integration/bun-types/fixture/wasm.ts
Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@github-actions github-actions Bot added the claude label Nov 9, 2026
@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:05 PM PT - Nov 10th, 2026

❌ @robobun, your commit 1f04f9b has some failures in Build #139998 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 48359

That installs a local version of the PR into your bun-48359 executable, so you can run:

bun-48359 --bun

@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: the fix is pushed. CI runs on the new head.

How I reproduced it (main at 89452d2, the project that bun init writes, tsc 7.0.2):

console.log(new Request(new URL("http://h.example/a")).url);
class S extends ReadableStream<string> {}
console.log(typeof S, typeof WebAssembly.Tag, typeof WebAssembly.Exception, typeof WebAssembly.JSTag);
  • bun index.ts prints http://h.example/a and function function function object.
  • tsc -p . and bun check report TS2769, TS2344 and three TS2339.
  • bun build --compile --aot index.ts stops with the same errors and writes no executable (debug build of main).

With the packages/bun-types of this branch the three commands report no error, the build exits 0, and the executable prints the same two lines.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread packages/bun-types/wasm.d.ts Resolved
Comment thread packages/bun-types/wasm.d.ts Resolved
Comment thread packages/bun-types/globals.d.ts Resolved
Comment thread packages/bun-types/wasm.d.ts Resolved
Comment thread packages/bun-types/globals.d.ts Resolved
Comment thread packages/bun-types/globals.d.ts Resolved
…odule import a tag

ReadableStream: the first version of the byte-source signatures changed
three things for a `new` expression: the expected type no longer set
the chunk type, `new ReadableStream<string>({ type: "bytes" })` was
accepted, and `InstanceType<typeof ReadableStream>` became
`ReadableStream<unknown>`. The signature that infers the chunk type now
has main's `R extends Uint8Array = Uint8Array<ArrayBuffer>` behind two
type parameters that an `extends` clause cannot fill, and it is the
last signature. The signature for an explicit `<R>` refuses a byte
source when `R` is not a `Uint8Array`. A `new` expression and the
derived types are as on main, and both `extends` forms compile.

WebAssembly: `Tag` has the `Symbol.toStringTag` member that the runtime
has, so a value that is not a tag is refused where a tag is expected.
`ExportValue` includes `Tag`, so a module can import and export one.
`toResizableBuffer()` and `toFixedLengthBuffer()` return
`ArrayBuffer | SharedArrayBuffer`, which is what a shared memory gives.

The `bun check` test reads the standard error of both commands through
the shared `run` helper.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

Comment thread packages/bun-types/globals.d.ts Resolved
Comment thread packages/bun-types/globals.d.ts Resolved
Comment thread packages/bun-types/globals.d.ts Resolved
… lib.dom's `WebAssembly.Tag`

`ReadableStream` has three construct signatures again, as on main, and
only the byte-source signature differs. Its chunk type `R` has no
constraint, so `extends ReadableStream<string>` and a bare
`extends ReadableStream` compile. A call without type arguments infers
the new type parameter `K` from `type: "bytes"`. Then the chunk type `B`
has the constraint `Uint8Array` and the default `Uint8Array<ArrayBuffer>`,
as on main, and an expected type sets it. An `extends` clause or an
explicit type argument leaves `K` as `never`, and the signature returns
`ReadableStream<R>`. The parameter is `UnderlyingByteSource` for each
`K`, so what a program reads off the constructor type stays as on main.

The second commit had two byte-source signatures. With four signatures
TypeScript 5.7 to 6.0 reports a rejected call through the last signature
only, a checked JavaScript file lost
`@extends {ReadableStream<Uint8Array>}`, and a union expected type did
not set the chunk type. Its conditional parameter refused
`new ReadableStream<T>(source)` with `T extends Uint8Array`.

A type argument on a byte source is not checked:
`new ReadableStream<string>({ type: "bytes" })` compiles, as it does with
lib.dom and with `@types/node`. Main refuses it.

`WebAssembly.Tag` keeps lib.dom's empty interface when lib.dom is loaded.
The `Symbol.toStringTag` member made a cast of an export to
`WebAssembly.Tag` an error there (TS2352). Without lib.dom the member
stays, and a tag stays among the values that a module imports.

`Memory.toFixedLengthBuffer` and `Memory.toResizableBuffer` are not
declared in this change any more. `traceStack` takes `undefined`.
… types of Bun

Without a tsconfig and without installed types, a build reads the
declarations that Bun brings, and Bun's `ReadableStream` constructor
applies there. So `class Pipe extends ReadableStream {}` no longer stops
that build with TS2510. The class moves from the programs that stop to
the programs that build and run, beside a subclass with a type argument.
@robobun
robobun marked this pull request as ready for review November 11, 2026 03:10

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants