如何修复 Readable.fromWeb(response.body) 类型报错?

最后更新于
import * as class streamstream from 'node:stream'

const const response: Responseresponse = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch('https://example.com')

if (const response: Responseresponse.Body.body: ReadableStream<Uint8Array<ArrayBuffer>> | nullbody) {
  class streamstream.class Stream.ReadableReadable.Stream.Readable.fromWeb(readableStream: ReadableStream, options?: Pick<stream.ReadableOptions, "encoding" | "highWaterMark" | "objectMode" | "signal">): stream.ReadablefromWeb(response.
Body.body: ReadableStream<Uint8Array<ArrayBuffer>>
body
)
Argument of type 'ReadableStream<Uint8Array<ArrayBuffer>>' is not assignable to parameter of type 'ReadableStream<any>'. Types of property 'pipeThrough' are incompatible. Type '<T>(transform: ReadableWritablePair<T, Uint8Array<ArrayBuffer>>, options?: StreamPipeOptions | undefined) => ReadableStream<...>' 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, Uint8Array<ArrayBuffer>>'. The types returned by 'readable.getReader(...).read(...)' are incompatible between these types. Type 'Promise<import("stream/web").ReadableStreamReadResult<T>>' is not assignable to type 'Promise<ReadableStreamReadResult<T>>'. Type 'import("stream/web").ReadableStreamReadResult<T>' is not assignable to type 'ReadableStreamReadResult<T>'. Type 'ReadableStreamReadDoneResult<T>' is not assignable to type 'ReadableStreamReadResult<T>'. Type 'import("stream/web").ReadableStreamReadDoneResult<T>' is not assignable to type 'ReadableStreamReadDoneResult<T>'. Property 'value' is optional in type 'ReadableStreamReadDoneResult<T>' but required in type 'ReadableStreamReadDoneResult<T>'.
}

观察 fetch 的类型,会发现他来自 lib.dom.d.ts,可是哪一行代码引入了 DOM 类型呢?

// tsconfig 里也没写 DOM ... 吗
{
  "compilerOptions": {
    "types": ["node"]
  }
}

其实,不写 "lib" 等于引入了默认 lib,也就是 typescript/lib/lib.d.ts

/// <reference lib="es5" />
/// <reference lib="dom" />
/// <reference lib="webworker.importscripts" />
/// <reference lib="scripthost" />

所以一个显而易见的修复方案是,手动指定一下 lib:

{
  "compilerOptions": {
    "lib": ["ES2024"], // <-- 没有 DOM
    "types": ["node"]
  }
}

但是这个办法只能在所有文件都是 Node.js 环境的才可以,实际项目可能组织成下面这样:

简单解释一下这个结构:{源码文件夹}/{独立模块}/{运行环境}/{代码}.ts
独立模块们可能是由不同人开发的,但是相比 monorepo 里开一堆包,这个结构不需要额外写一大堆内容大部分重复的 package.json,也不需要在每个子包里精确管理依赖(只要最顶层这个包的依赖完整即可)。
根据运行环境分隔代码的好处是,开发者可以编写简单的 lint 插件 避免例如前端的代码引用了后端的问题。需要注意的是这种风格下最好避免使用 Barrel files (index.ts)。

这种情况下就不能避免 DOM 类型被引入了,解法是我们强行让 DOM 里的 Response 兼容 Node.js 里的:

import * as class streamstream from 'node:stream'

// 注意到 DOM 里的 Response 是全局的, 可以通过 declare global 来修改他
import * as module "stream/web"streamWeb from 'stream/web'
declare global {
  interface Response {
    Response.body: streamWeb.ReadableStream<Uint8Array<ArrayBufferLike>> | nullbody: module "stream/web"streamWeb.interface ReadableStream<R = any>ReadableStream<interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>Uint8Array> | null
  }
}

const const response: Responseresponse = await function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)fetch('https://example.com')

if (const response: Responseresponse.Response.body: streamWeb.ReadableStream<Uint8Array<ArrayBufferLike>> | nullbody) {
  class streamstream.class Stream.ReadableReadable.Stream.Readable.fromWeb(readableStream: streamWeb.ReadableStream, options?: Pick<stream.ReadableOptions, "encoding" | "highWaterMark" | "objectMode" | "signal">): stream.ReadablefromWeb(const response: Responseresponse.
Response.body: streamWeb.ReadableStream<Uint8Array<ArrayBufferLike>>
body
)
}