如何修复 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>'. The types of 'getReader(...).read' are incompatible between these types. Type '<T extends Exclude<BufferSource, ArrayBuffer>>(view: T, options?: ReadableStreamBYOBReaderReadOptions | undefined) => Promise<ReadableStreamReadResult<T>>' is not assignable to type '<T extends ArrayBufferView>(view: T, options?: { min?: number | undefined; } | undefined) => Promise<ReadableStreamReadResult<T>>'. Types of parameters 'view' and 'view' are incompatible. Type 'T' is not assignable to type 'ArrayBufferView<ArrayBuffer>'. Type 'ArrayBufferView<ArrayBufferLike>' is not assignable to type 'ArrayBufferView<ArrayBuffer>'. Type 'ArrayBufferLike' is not assignable to type 'ArrayBuffer'. Type 'SharedArrayBuffer' is missing the following properties from type 'ArrayBuffer': resizable, resize, detached, transfer, transferToFixedLength
}

观察 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
)
}