Skip to content

bun-types: drop undefined from the Null* subprocess aliases where the default is "pipe" - #39303

Open
robobun wants to merge 1 commit into
mainfrom
farm/fb70be46/null-subprocess-undefined-stdout
Open

bun-types: drop undefined from the Null* subprocess aliases where the default is "pipe"#39303
robobun wants to merge 1 commit into
mainfrom
farm/fb70be46/null-subprocess-undefined-stdout

Conversation

@robobun

@robobun robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • NullSubprocess and NullSyncSubprocess (packages/bun-types/bun.d.ts:8161 and :8169 on main) list undefined in every stdio type argument, so NullSubprocess["stdout"] is ReadableStream<Uint8Array<ArrayBuffer>> | undefined and NullSyncSubprocess["stdout"] / ["stderr"] are Buffer | undefined, not undefined.
  • undefined in a stdio option means the slot's default, and the default is "pipe" for Bun.spawn's stdout and for both Bun.spawnSync slots, so Bun.spawn(cmd, { stdio: ["ignore", undefined, "ignore"] }) has a live ReadableStream on .stdout and Bun.spawnSync(cmd, { stdio: ["ignore", undefined, undefined] }) has two Buffers, yet both are accepted as NullSubprocess / NullSyncSubprocess.
  • The other slots are fine: Bun.spawn defaults stdin to "ignore" and stderr to "inherit", so undefined there leaves nothing on the process.

Fix

  • Removes undefined from the three type arguments whose slot defaults to "pipe": NullSubprocess's stdout, and both of NullSyncSubprocess's. The stdin and stderr arguments of NullSubprocess still accept undefined. The JSDoc of both aliases now says which options qualify and why undefined only counts in some slots.
  • Correct because the aliases now list exactly the options for which the runtime exposes nothing on the process (checked with bun 1.4.0, output in the details below), and because the mapping types ReadableToIO / ReadableToSyncIO already turn an undefined option in these slots into a stream / Buffer: with the change, all three properties become exactly undefined, and a Subprocess / SyncSubprocess with a "pipe"-by-default slot no longer satisfies the alias.
  • The only code that stops compiling is code assigning a process with an explicitly undefined stdout (or spawnSync stderr) to a Null* alias, which is the case the alias mistyped. Nothing in this repo uses the aliases outside the fixture, and the docs do not mention them.
  • Verified by test/integration/bun-types/fixture/spawn.ts (new assertions after the existing NullSubprocess ones): the three properties are undefined, undefined is still accepted in Bun.spawn's stdin and stderr slots, and the three "pipe"-by-default cases are rejected via @ts-expect-error. Against the unfixed bun.d.ts the new lines produce 6 diagnostics (3 wrong property types, 3 unused @ts-expect-error); with the fix, zero.
  • bun bd test test/integration/bun-types/bun-types.test.ts: the new Bun.spawn > fixture/spawn.ts type-checks case fails with those 6 diagnostics when packages/ is stashed and passes with the fix.
  • USE_SYSTEM_BUN=1 bun test test/integration/bun-types/bun-types.test.ts: 16 pass, including the lib.dom run, whose pinned spawn.ts:62 / :107 diagnostics are unchanged (the new lines are below them).
  • The added test case in bun-types.test.ts is byte-identical to the one in bun-types: declare Subprocess.writable and lint interface Subprocess against the classes.ts table #39279, bun-types: type Subprocess stdio properties by what the runtime exposes for each stdio option #39283 and bun-types: type Subprocess.stderr as undefined when the stderr option is undefined #39297, so whichever lands first, the rest merge cleanly; git merge-tree of this branch with each of those (and with test(bun-types): replace the tsgo and Bun.mmap spawns with one whole-fixture tsc run, enforced by a lint #39270) reports no conflicts, and fixture/spawn.ts still type-checks in each merged tree. bun-types: type Subprocess.stderr as undefined when the stderr option is undefined #39297's NullSubprocess["stderr"] assertion relies on stderr keeping undefined, which this PR preserves.

Background

  • Bun.spawn returns Subprocess<In, Out, Err> and Bun.spawnSync returns SyncSubprocess<Out, Err>, where the type arguments are the literal types of the stdio options passed. proc.stdout is typed as ReadableToIO<Out> (a ReadableStream for "pipe", a number for an fd, otherwise undefined), and SyncSubprocess.stdout as ReadableToSyncIO<Out> (Buffer for "pipe", otherwise undefined). Both map an undefined option like "pipe".
  • NullSubprocess, PipedSubprocess, NullSyncSubprocess and friends are aliases that instantiate those interfaces with a union of options, for holding processes whose exact configuration is not known statically. Because the mapping types distribute over the union, each property of the alias is the union of what every listed option produces, and a process is assignable to the alias when its own options (or what they produce) fit in those unions.
  • An option that is undefined (explicitly, or through a union such as a stdio tuple with a conditional entry) leaves the runtime default for that slot: ["ignore", "pipe", "inherit"] for Bun.spawn and ["ignore", "pipe", "pipe"] for Bun.spawnSync (src/runtime/api/bun/js_bun_spawn_bindings.rs, the @default lines on stdio / stdout / stderr in bun.d.ts). Omitting the option altogether does not produce an undefined type argument: the spawn signatures default the type parameters to those same values.
Runtime check of the defaults (bun 1.4.0)
spawn  [ignore, undefined, ignore]    -> stdin undefined, stdout ReadableStream, stderr undefined
spawn  [undefined, ignore, undefined] -> stdin undefined, stdout undefined,      stderr undefined
spawn  [undefined, undefined, undefined] -> stdin undefined, stdout ReadableStream, stderr undefined
spawnSync [ignore, undefined, undefined] -> stdout Buffer,    stderr Buffer
spawnSync [ignore, undefined, ignore]    -> stdout Buffer,    stderr undefined
spawnSync [ignore, ignore, undefined]    -> stdout undefined, stderr Buffer
spawnSync [ignore, ignore, inherit]      -> stdout undefined, stderr undefined
spawnSync [null, null, null]             -> stdout undefined, stderr undefined
tsc 6.0.2 over fixture/spawn.ts against the unfixed bun.d.ts
spawn.ts(192,44): error TS2554: Expected 2 arguments, but got 0.      // NullSubprocess["stdout"] is not undefined
spawn.ts(193,52): error TS2554: Expected 2 arguments, but got 0.      // NullSyncSubprocess["stdout"]
spawn.ts(194,52): error TS2554: Expected 2 arguments, but got 0.      // NullSyncSubprocess["stderr"]
spawn.ts(197,1): error TS2578: Unused '@ts-expect-error' directive.   // spawn stdio ["ignore", undefined, "ignore"] accepted as NullSubprocess
spawn.ts(201,1): error TS2578: Unused '@ts-expect-error' directive.   // spawnSync stdio ["ignore", undefined, "ignore"] accepted as NullSyncSubprocess
spawn.ts(203,1): error TS2578: Unused '@ts-expect-error' directive.   // spawnSync stdio ["ignore", "ignore", undefined] accepted as NullSyncSubprocess

… default is "pipe"

An undefined stdio option means the slot's default. Bun.spawn defaults
stdout to "pipe" and Bun.spawnSync defaults both stdout and stderr to
"pipe", so a process spawned with undefined in one of those slots has a
live ReadableStream or a Buffer there, and NullSubprocess["stdout"],
NullSyncSubprocess["stdout"] and NullSyncSubprocess["stderr"] came out
as ReadableStream | undefined and Buffer | undefined instead of
undefined. Remove undefined from those three type arguments. stdin and
stderr of Bun.spawn default to "ignore" and "inherit", so undefined
stays allowed there.

The bun-types test gains a tsc run over fixture/spawn.ts that also runs
on debug builds, where the in-process type checks are skipped.
@robobun
robobun requested a review from alii as a code owner August 16, 2026 04:43
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a9df4c51-4dc0-4f34-b1ce-cd5024b7f11a

📥 Commits

Reviewing files that changed from the base of the PR and between 83d65fa and cb7ecd2.

📒 Files selected for processing (3)
  • packages/bun-types/bun.d.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/spawn.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:12 PM PT - Aug 15th, 2026

@robobun, your commit cb7ecd2496ab13f879a2a4ac8226e4b2acf0ec52 passed in Build #99139! 🎉


🧪   To try this PR locally:

bunx bun-pr 39303

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

bun-39303 --bun

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. Bot review had no findings; self-review is in progress and CI is running.

Reproduced with tsc 6.0.2 against main's bun.d.ts: the new assertions in test/integration/bun-types/fixture/spawn.ts report NullSubprocess["stdout"] as ReadableStream<Uint8Array<ArrayBuffer>> | undefined and both NullSyncSubprocess slots as Buffer | undefined, and accept a spawn / spawnSync call with an undefined stdout (or spawnSync stderr) slot as a Null* process (6 diagnostics). At runtime those slots hold a ReadableStream / Buffer, since undefined selects the "pipe" default there.

With the fix, bun bd test test/integration/bun-types/bun-types.test.ts and the release-mode run (16 cases, including the lib.dom configuration) both pass; with packages/ stashed, the new fixture/spawn.ts type-checks case fails with those 6 diagnostics.

Related open bun-types spawn PRs: #39279, #39283, #39297 (same added test case, byte-identical) and #39270. This branch merges cleanly with each, and fixture/spawn.ts type-checks in each merged tree.

@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.

LGTM — tightens the Null* subprocess aliases so their stdout/stderr slots resolve to exactly undefined.

What was reviewed:

  • Verified ReadableToIO / ReadableToSyncIO map an undefined option to a stream / Buffer, and that the spawn / spawnSync type-parameter defaults are ["ignore","pipe","inherit"] and ["ignore","pipe","pipe"] — so dropping undefined from exactly the three "pipe"-default slots is correct.
  • Checked the zero-arg expectType<T>().is<U>() overload in fixture/utilities.ts supports the new assertions, and that the new fixture lines land below the pinned spawn.ts:62 / :107 lib.dom diagnostics.
  • Grepped for NullSubprocess / NullSyncSubprocess — no in-repo consumers outside the fixture.
Extended reasoning...

Overview

This PR touches three files: packages/bun-types/bun.d.ts (removes undefined from three type arguments in the NullSubprocess and NullSyncSubprocess aliases and expands their JSDoc), test/integration/bun-types/fixture/spawn.ts (17 new lines of type assertions covering the property types and positive/negative assignability), and test/integration/bun-types/bun-types.test.ts (a new Bun.spawn > fixture/spawn.ts type-checks case that spawns tsc over the fixture so it also runs on debug builds).

Security risks

None. This is a pure .d.ts change plus type-check fixture assertions; no runtime code, no native code, no I/O changes.

Level of scrutiny

Low. The change narrows two convenience type aliases to match documented and observed runtime behavior. I cross-checked the claims against the source: ReadableToIO<X> and ReadableToSyncIO<X> at bun.d.ts:7439/:7445 treat undefined like "pipe"; the spawn overloads at :7714/:7749 default Out to "pipe" and Err to "inherit"; the spawnSync overloads at :7786/:7822 default both to "pipe". So the previous undefined in NullSubprocess's stdout slot and both NullSyncSubprocess slots did widen those properties to ReadableStream | undefined / Buffer | undefined, and removing it makes them exactly undefined while keeping undefined in the stdin/stderr slots whose defaults already produce undefined. The reasoning in the PR description is thorough and matches the code.

Other factors

  • The fixture additions include both directions: exact property-type checks via expectType<...>().is<undefined>(), positive expectAssignable cases for undefined in the stdin/stderr slots, and @ts-expect-error for the three "pipe"-default slots. I confirmed the zero-arg expectType<T>() overload exists in fixture/utilities.ts.
  • The new fixture lines are appended after line 187, so the two pinned lib.dom diagnostics at spawn.ts:62 / :107 in the "checks with lib.dom.d.ts" case are unaffected.
  • NullSubprocess / NullSyncSubprocess have no in-repo consumers outside the fixture (grepped), so no other code needs updating.
  • The added bun-types.test.ts case follows the exact pattern of the neighboring Bun.mmap case (same tsc spawn shape, same Promise.all pipe drain, stderr/stdout/exitCode assertion order) and per the description is byte-identical to the one in three sibling PRs to avoid merge conflicts.

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.

1 participant