(一昔前の)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
、TypeScriptだと Promise
となっています。
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
、TypeScriptだと Promise
となっています。これはプロパティのみ差分があることから、Node.jsではプロパティの場合はvoid
を避けるようにしていると予想できます。
ReadableStreamBYOBReader
の差分についてはTypeScriptの型の採用条件が関係するようです。
lib.dom.d.tsがどのように更新されるか調べてみたという記事はTypeScriptの型定義の更新に関する調査の記事です。これによると
MDN の Browser compatibility を基準にしていて、2 つ以上のブラウザエンジンがサポートすれば、自動的に型が追加されるみたいです。
ということです。実際ReadableStreamBYOBReader#readメソッドを見てみると、第2引数のoptions.minはFirefoxのみサポートされています。そのため、TypeScriptにはありませんでした。
また別のタイミングで時間を取って、頑張って型エラーを排除して型とより仲良くなろうと思います。
tsconfig.json
のlib
は特に設定無しで行っています。 ↩︎