to-readable-stream
to-readable-stream wraps one already available JavaScript value in a WHATWG ReadableStream. The stream enqueues that exact value as its only chunk and then closes, with generic TypeScript inference preserving the chunk type. Strings stay strings, Uint8Arrays stay bytes, and objects stay objects. Despite the name, it does not create a Node stream.Readable, split large values, encode text, iterate collections, or lazily pull from a source. It is a four-line convenience for APIs that specifically accept web streams.
Use it only when one in-memory value must satisfy a web ReadableStream parameter and preserving that exact chunk type is desirable. For almost everything else, a direct ReadableStream constructor or the native Node stream conversion APIs are clearer and avoid a dependency.
Use it if
- You have one value in memory and an API specifically requires a WHATWG ReadableStream
- You want the emitted chunk to retain its exact JavaScript and TypeScript type
- You are on Node 18 or newer or a browser with the global ReadableStream constructor, and your project uses ESM
- You need a Node stream.Readable: the README explicitly says not to confuse the two and recommends stream.Readable.from() for Node streams
- You need actual streaming of a large value: the implementation enqueues the entire supplied value once during start and immediately closes, so there is no chunking or lazy production
- Your consumer requires bytes but your input is text: the generic declaration returns ReadableStream<Value> and the source does no TextEncoder conversion, so a string remains a string chunk
- You need CommonJS or Node older than 18: version 4 declares type module, exports only the ESM default, and sets Node >=18 in package metadata
- You need cancellation, producer errors, backpressure-aware pulling, iterable support, or multiple chunks: the public API accepts one value and exposes no options or source callbacks
Setup reality
Install the package and default-import it from an ES module. Version 4 is ESM-only, has no runtime dependencies, includes a generic TypeScript declaration, and declares Node 18 or newer. It relies on the global WHATWG ReadableStream constructor; it does not import a ponyfill. The critical setup decision is chunk type. The function does not convert anything. toReadableStream('hello') produces one string chunk, not UTF-8 bytes. That is fine for your own reader or a TransformStream that expects strings, but byte-oriented consumers such as Node's Fetch implementation can reject a non-Uint8Array chunk. Encode text first with TextEncoder when feeding Response, compression, hashing, file, or protocol APIs that expect bytes. Buffer works in Node because Buffer extends Uint8Array, but Buffer is not a browser global. Objects are accepted and typed, yet only custom object-mode web-stream consumers know what to do with them. The input is already fully materialized before the stream exists, and start enqueues it immediately, so wrapping a large Buffer does not lower peak memory or add useful backpressure. Cancellation cannot stop upstream work because there is no upstream work. Each call makes a fresh one-shot stream; once a reader locks or consumes it, create another stream or call tee before reading when two consumers need the same chunk. If you need a Node Readable, use Readable.from as the README recommends. If you need several chunks, an iterable, an async producer, or a pull/cancel implementation, construct ReadableStream directly or choose the related into-stream package for a richer Node-stream conversion. For Node-to-web interoperability, Readable.toWeb and Readable.fromWeb are clearer because they state which stream model crosses the boundary.
Patterns
Wrap a string as one chunkstream-string-chunk
import toReadableStream from 'to-readable-stream'
const stream = toReadableStream('hello')
const {value, done} = await stream.getReader().read()
console.log(value, done)value is the string hello and done is false on the first read. A second read returns done true. No text encoding occurs.
Encode text before creating a byte streamstream-utf8-bytes
import toReadableStream from 'to-readable-stream'
const bytes = new TextEncoder().encode('hello')
const stream = toReadableStream(bytes)Encode explicitly for consumers that require Uint8Array chunks. The package never converts string chunks to bytes.
Wrap a Node Bufferstream-node-buffer
import toReadableStream from 'to-readable-stream'
const stream = toReadableStream(Buffer.from('hello'))Buffer is a Uint8Array subclass in Node, so it works for byte consumers. Use TextEncoder for browser-portable code.
Emit a typed objectstream-object-chunk
import toReadableStream from 'to-readable-stream'
const stream = toReadableStream({id: 1, name: 'Ada'})
const reader = stream.getReader()
const {value} = await reader.read()
console.log(value.name)The object is emitted by reference and is not cloned or serialized. Only custom consumers should expect object chunks.
Read the one-shot stream manuallyread-to-completion
const reader = toReadableStream(value).getReader()
try {
while (true) {
const result = await reader.read()
if (result.done) break
consume(result.value)
}
} finally {
reader.releaseLock()
}There is exactly one value, but the normal reader loop keeps code compatible with other ReadableStream sources.
Consume with async iterationiterate-stream
for await (const chunk of toReadableStream(value)) {
console.log(chunk)
}Modern Node web streams support async iteration. Some older browser type libraries may not expose Symbol.asyncIterator even when getReader works.
Decode an emitted byte chunkdecode-byte-stream
const bytes = new TextEncoder().encode('hello')
const textStream = toReadableStream(bytes).pipeThrough(new TextDecoderStream())
for await (const text of textStream) {
console.log(text)
}TextDecoderStream expects byte chunks. Passing toReadableStream('hello') would provide the wrong chunk type.
Use bytes as a Response bodycreate-fetch-response
const body = toReadableStream(new TextEncoder().encode('hello'))
const response = new Response(body, {
headers: {'content-type': 'text/plain; charset=utf-8'}
})Use Uint8Array chunks for Fetch-compatible bodies. A raw string chunk can be rejected by byte-oriented Response consumers.
Split before consumingtee-two-consumers
const source = toReadableStream(bytes)
const [forHashing, forUpload] = source.tee()ReadableStream is one-shot once consumed. Call tee before acquiring a reader when two consumers need the value.
Pipe the value to a WritableStreampipe-to-sink
const received = []
const sink = new WritableStream({
write(chunk) {
received.push(chunk)
}
})
await toReadableStream(value).pipeTo(sink)pipeTo receives the original value as one write call. The wrapper does not divide it according to sink backpressure.
Convert byte chunks to a Node Readablebridge-to-node-stream
import {Readable} from 'node:stream'
import toReadableStream from 'to-readable-stream'
const webStream = toReadableStream(new TextEncoder().encode('hello'))
const nodeStream = Readable.fromWeb(webStream)If the final consumer wants Node streams, Readable.from([value]) is usually simpler, as the package README recommends.
Preserve the chunk type in TypeScriptpreserve-typescript-type
import toReadableStream from 'to-readable-stream'
type Event = {type: 'created'; id: string}
const event: Event = {type: 'created', id: '42'}
const stream: ReadableStream<Event> = toReadableStream(event)The generic declaration infers the exact input type. It does not constrain values to strings, Buffer, or Uint8Array.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| into-stream | npm | You need a richer Node stream converter for strings, promises, arrays, iterables, async iterables, buffers, typed arrays, or objects |
| web-streams-polyfill | npm | Your runtime lacks WHATWG streams and you need the constructors before building a stream directly |
| readable-stream | npm | Your consumer expects the Node streams API and you need the userland implementation across Node versions |
| streamx | npm | You want an alternative Node-style stream implementation with a broader producer and transform model |