(一昔前の)Node.jsといえばStream。そんなStreamも今ではWHATWGのよるWeb Standardsとして各ブラウザにStream APIが生えています。Node.jsのReadableなStreamをWeb StandardsのReadableStreamに変換するにはStream.Readable.toWebという静的メソッドを利用することで変換できます。
型定義に関しては、Web Standardsにもなっていますので当然TypeScriptのlid.dom.tsに型定義があります。Node.jsはNode.jsで自前で@types/nodeの型定義を配布していて、自前のWeb版のReadableStreamの型定義があります。ここで本題。これらNode.jsとTypeScriptの2つの型に互換性がありません。
例えば次のようなコードを書いてtscでビルドしてみます[1]。
import Stream from "node:stream";
import { ReadableStream as nodejs_ReadableStream } from "node:stream/web"
const nodejsReadable = Stream.Readable.from("Hello World");
const convertedToNodejs: nodejs_ReadableStream= Stream.Readable.toWeb(nodejsReadable);
const convertedToLibDomJs: ReadableStream = Stream.Readable.toWeb(nodejsReadable);
当然convertedToNodejsはNode.js内部での型変換なので問題ないですが、TypeScript側の型へ代入する部分では型エラーが発生します。
index.ts:6:7 - error TS2322: Type 'import("stream/web").ReadableStream<any>' is not assignable to type 'ReadableStream<any>'.
  Types of property 'pipeThrough' are incompatible.
    Type '<T>(transform: import("stream/web").ReadableWritablePair<T, any>, options?: import("stream/web").StreamPipeOptions | undefined) => import("stream/web").ReadableStream<T>' is not assignable to type '<T>(transform: ReadableWritablePair<T, any>, options?: StreamPipeOptions | undefined) => ReadableStream<T>'.
      Types of parameters 'transform' and 'transform' are incompatible.
        Type 'ReadableWritablePair<T, any>' is not assignable to type 'ReadableWritablePair<T | undefined, any>'.
          Types of property 'readable' are incompatible.
            Type 'ReadableStream<T>' is missing the following properties from type 'ReadableStream<T | undefined>': values, [Symbol.asyncIterator]
6 const convertedToLibDomJs: ReadableStream  = Stream.Readable.toWeb(nodejsReadable)
上記の型定義を紐解いていきます。
以降、バージョンは @types/node が 22.14.0、typescript が 5.8.3 となっています。
ちなみに結論としてはこの型エラーはどうしようもないので、型アサーション等で対応しましょう。型アサーションで特筆するほどの問題は発生しないはずです。Issueも立っていたりします。
型定義の差分
tscでビルドを実行して、型エラーをヒントにlid.dom.tsと@types/nodeの定義を目grepする旅に出ました。下記が差分です。
続いて、ReadableStreamGenericReader および WritableStreamDefaultWriter の型定義に多少のズレがあります。closed や ready の戻り値が、Node.jsだと Promise<undefined>、TypeScriptだと Promise<void> となっています。
ReadableStreamGenericReaderの定義は下記の通り。
    interface ReadableStreamGenericReader {
        readonly closed: Promise<undefined>;
        cancel(reason?: any): Promise<void>;
    }
  interface ReadableStreamGenericReader {
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/closed) */
    readonly closed: Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/cancel) */
    cancel(reason?: any): Promise<void>;
}
  WritableStreamDefaultWriterの定義は下記の通り。
    interface WritableStreamDefaultWriter<W = any> {
        readonly closed: Promise<undefined>;
        readonly desiredSize: number | null;
        readonly ready: Promise<undefined>;
        abort(reason?: any): Promise<void>;
        close(): Promise<void>;
        releaseLock(): void;
        write(chunk?: W): Promise<void>;
    }
  interface WritableStreamDefaultWriter<W = any> {
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/closed) */
    readonly closed: Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/desiredSize) */
    readonly desiredSize: number | null;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/ready) */
    readonly ready: Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/abort) */
    abort(reason?: any): Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/close) */
    close(): Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/releaseLock) */
    releaseLock(): void;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/WritableStreamDefaultWriter/write) */
    write(chunk?: W): Promise<void>;
}
  その他、下記のように差分があったりします。
ReadableStreamに values および Symbol.asyncIterator の有無があります。ちなみに、これは DOM.AsyncIterable を tsconfig.json の lib にいれるとTypeScriptに生えます。
    interface ReadableStream<R = any> {
        readonly locked: boolean;
        cancel(reason?: any): Promise<void>;
        getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
        getReader(): ReadableStreamDefaultReader<R>;
        getReader(options?: ReadableStreamGetReaderOptions): ReadableStreamReader<R>;
        pipeThrough<T>(transform: ReadableWritablePair<T, R>, options?: StreamPipeOptions): ReadableStream<T>;
        pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;
        tee(): [ReadableStream<R>, ReadableStream<R>];
        values(options?: { preventCancel?: boolean }): ReadableStreamAsyncIterator<R>;
        [Symbol.asyncIterator](): ReadableStreamAsyncIterator<R>;
    }
  interface ReadableStream<R = any> {
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/locked) */
    readonly locked: boolean;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/cancel) */
    cancel(reason?: any): Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/getReader) */
    getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
    getReader(): ReadableStreamDefaultReader<R>;
    getReader(options?: ReadableStreamGetReaderOptions): ReadableStreamReader<R>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeThrough) */
    pipeThrough<T>(transform: ReadableWritablePair<T, R>, options?: StreamPipeOptions): ReadableStream<T>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/pipeTo) */
    pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStream/tee) */
    tee(): [ReadableStream<R>, ReadableStream<R>];
}
  ReadableStreamBYOBReaderについての差分。こちらは型エラーになりません。
    interface ReadableStreamBYOBReader extends ReadableStreamGenericReader {
        /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) */
        read<T extends ArrayBufferView>(
            view: T,
            options?: {
                min?: number;
            },
        ): Promise<ReadableStreamReadResult<T>>;
        /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) */
        releaseLock(): void;
    }
  interface ReadableStreamBYOBReader extends ReadableStreamGenericReader {
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/read) */
    read<T extends ArrayBufferView>(view: T): Promise<ReadableStreamReadResult<T>>;
    /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ReadableStreamBYOBReader/releaseLock) */
    releaseLock(): void;
}
  上記でNode.jsの型をほぼTypeScriptと同じにしたんですが、なぜか全然型エラーが治らない(TがT | undefinedになる)ので、一旦諦めてなんで上記のような挙動になるのか推測しました。
差分の考察
closed や ready の戻り値が、Node.jsだと Promise<undefined>、TypeScriptだと Promise<void> となっています。これはプロパティのみ差分があることから、Node.jsではプロパティの場合はvoidを避けるようにしていると予想できます。
ReadableStreamBYOBReader の差分についてはTypeScriptの型の採用条件が関係するようです。
lib.dom.d.tsがどのように更新されるか調べてみたという記事はTypeScriptの型定義の更新に関する調査の記事です。これによると
MDN の Browser compatibility を基準にしていて、2 つ以上のブラウザエンジンがサポートすれば、自動的に型が追加されるみたいです。
ということです。実際ReadableStreamBYOBReader#readメソッドを見てみると、第2引数のoptions.minはFirefoxのみサポートされています。そのため、TypeScriptにはありませんでした。
また別のタイミングで時間を取って、頑張って型エラーを排除して型とより仲良くなろうと思います。
tsconfig.jsonのlibは特に設定無しで行っています。 ↩︎
