TypeScript/JavaScript

Node.js(の@types)とTypeScript(lib.dom.ts)でReadableStreamの型定義が違う

(一昔前の)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]

index.ts
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/node22.14.0typescript5.8.3 となっています。

ちなみに結論としてはこの型エラーはどうしようもないので、型アサーション等で対応しましょう。型アサーションで特筆するほどの問題は発生しないはずです。Issueも立っていたりします

型定義の差分

tscでビルドを実行して、型エラーをヒントにlid.dom.tsと@types/nodeの定義を目grepする旅に出ました。下記が差分です。

続いて、ReadableStreamGenericReader および WritableStreamDefaultWriter の型定義に多少のズレがあります。closedready の戻り値が、Node.jsだと Promise、TypeScriptだと Promise となっています。

ReadableStreamGenericReaderの定義は下記の通り。

99 100 101 102
interface ReadableStreamGenericReader { readonly closed: Promise<undefined>; cancel(reason?: any): Promise<void>; }

19634 19635 19636 19637 19638 19639
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の定義は下記の通り。

309 310 311 312 313 314 315 316 317
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>; }

27390 27391 27392 27393 27394 27395 27396 27397 27398 27399 27400 27401 27402 27403 27404 27405
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>; }

その他、下記のように差分があったりします。

ReadableStreamvalues および Symbol.asyncIterator の有無があります。ちなみに、これは DOM.AsyncIterabletsconfig.jsonlib にいれるとTypeScriptに生えます。

173 174 175 176 177 178 179 180 181 182 183 184
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>; }

19552 19553 19554 19555 19556 19557 19558 19559 19560 19561 19562 19563 19564 19565 19566 19567
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についての差分。こちらは型エラーになりません。

206 207 208 209 210 211 212 213 214 215 216
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; }

19577 19578 19579 19580 19581 19582
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になる)ので、一旦諦めてなんで上記のような挙動になるのか推測しました。

差分の考察

closedready の戻り値が、Node.jsだと Promise、TypeScriptだと Promise となっています。これはプロパティのみ差分があることから、Node.jsではプロパティの場合はvoidを避けるようにしていると予想できます。

ReadableStreamBYOBReader の差分についてはTypeScriptの型の採用条件が関係するようです。

lib.dom.d.tsがどのように更新されるか調べてみたという記事はTypeScriptの型定義の更新に関する調査の記事です。これによると

MDN の Browser compatibility を基準にしていて、2 つ以上のブラウザエンジンがサポートすれば、自動的に型が追加されるみたいです。

ということです。実際ReadableStreamBYOBReader#readメソッドを見てみると、第2引数のoptions.minはFirefoxのみサポートされています。そのため、TypeScriptにはありませんでした。

また別のタイミングで時間を取って、頑張って型エラーを排除して型とより仲良くなろうと思います。


  1. tsconfig.jsonlib は特に設定無しで行っています。 ↩︎