mwopitz
mwopitz

Reputation: 615

How to extend a Node.js transform stream type-safe with custom events?

I'm trying to implement a transform stream with Node.js and Typescript that emits custom events while processing data. But I'm struggling to get the typing right.

I'm using Node.js 22, typescript 5.7.2, and @types/node 22.10.2.

Here's a rough sketch of the code:

import { Transform, type TransformCallback } from "node:stream";

export interface CustomEventData {
  foo: "bar" | "baz"
}

export class CustomTransform extends Transform {
  constructor() {
    super();
  }

  _transform(
    chunk: Buffer | string,
    encoding: BufferEncoding,
    callback: TransformCallback,
  ): void {
    const data: CustomEventData = { foo: "bar" };
    this.emit("custom-event", data);
    callback(null, chunk);
  }
}

The goal is to have TypeScript automatically detect the type of the data parameter in the following scenario as CustomEventData:

const myTransform = new CustomTransform();
myTransform.on("custom-event", (data) => {
  console.log("received custom-event with data", data);
});

The only way I managed to make this work is by using declare interface as seen below:

declare interface CustomTransform extends Transform {
  addListener(event: "custom-event", listener: (data: CustomEventData) => void): this;
  addListener(event: "close", listener: () => void): this;
  addListener(event: "data", listener: (chunk: any) => void): this;
  addListener(event: "drain", listener: () => void): this;
  addListener(event: "end", listener: () => void): this;
  addListener(event: "error", listener: (err: Error) => void): this;
  addListener(event: "finish", listener: () => void): this;
  addListener(event: "pause", listener: () => void): this;
  addListener(event: "pipe", listener: (src: Readable) => void): this;
  addListener(event: "readable", listener: () => void): this;
  addListener(event: "resume", listener: () => void): this;
  addListener(event: "unpipe", listener: (src: Readable) => void): this;
  emit(event: "custom-event", data: CustomEventData): boolean;
  emit(event: "close"): boolean;
  emit(event: "data", chunk: any): boolean;
  emit(event: "drain"): boolean;
  emit(event: "end"): boolean;
  emit(event: "error", err: Error): boolean;
  emit(event: "finish"): boolean;
  emit(event: "pause"): boolean;
  emit(event: "pipe", src: Readable): boolean;
  emit(event: "readable"): boolean;
  emit(event: "resume"): boolean;
  emit(event: "unpipe", src: Readable): boolean;
  on(event: "custom-event", listener: (data: CustomEventData) => void): this;
  on(event: "close", listener: () => void): this;
  on(event: "data", listener: (chunk: any) => void): this;
  on(event: "drain", listener: () => void): this;
  on(event: "end", listener: () => void): this;
  on(event: "error", listener: (err: Error) => void): this;
  on(event: "finish", listener: () => void): this;
  on(event: "pause", listener: () => void): this;
  on(event: "pipe", listener: (src: Readable) => void): this;
  on(event: "readable", listener: () => void): this;
  on(event: "resume", listener: () => void): this;
  on(event: "unpipe", listener: (src: Readable) => void): this;
  once(event: "custom-event", listener: (data: CustomEventData) => void): this;
  once(event: "close", listener: () => void): this;
  once(event: "data", listener: (chunk: any) => void): this;
  once(event: "drain", listener: () => void): this;
  once(event: "end", listener: () => void): this;
  once(event: "error", listener: (err: Error) => void): this;
  once(event: "finish", listener: () => void): this;
  once(event: "pause", listener: () => void): this;
  once(event: "pipe", listener: (src: Readable) => void): this;
  once(event: "readable", listener: () => void): this;
  once(event: "resume", listener: () => void): this;
  once(event: "unpipe", listener: (src: Readable) => void): this;
  prependListener(event: "custom-event", listener: (data: CustomEventData) => void): this;
  prependListener(event: "close", listener: () => void): this;
  prependListener(event: "data", listener: (chunk: any) => void): this;
  prependListener(event: "drain", listener: () => void): this;
  prependListener(event: "end", listener: () => void): this;
  prependListener(event: "error", listener: (err: Error) => void): this;
  prependListener(event: "finish", listener: () => void): this;
  prependListener(event: "pause", listener: () => void): this;
  prependListener(event: "pipe", listener: (src: Readable) => void): this;
  prependListener(event: "readable", listener: () => void): this;
  prependListener(event: "resume", listener: () => void): this;
  prependListener(event: "unpipe", listener: (src: Readable) => void): this;
  prependOnceListener(event: "custom-event", listener: (data: CustomEventData) => void): this;
  prependOnceListener(event: "close", listener: () => void): this;
  prependOnceListener(event: "data", listener: (chunk: any) => void): this;
  prependOnceListener(event: "drain", listener: () => void): this;
  prependOnceListener(event: "end", listener: () => void): this;
  prependOnceListener(event: "error", listener: (err: Error) => void): this;
  prependOnceListener(event: "finish", listener: () => void): this;
  prependOnceListener(event: "pause", listener: () => void): this;
  prependOnceListener(event: "pipe", listener: (src: Readable) => void): this;
  prependOnceListener(event: "readable", listener: () => void): this;
  prependOnceListener(event: "resume", listener: () => void): this;
  prependOnceListener(event: "unpipe", listener: (src: Readable) => void): this;
  removeListener(event: "custom-event", listener: (data: CustomEventData) => void): this;
  removeListener(event: "close", listener: () => void): this;
  removeListener(event: "data", listener: (chunk: any) => void): this;
  removeListener(event: "drain", listener: () => void): this;
  removeListener(event: "end", listener: () => void): this;
  removeListener(event: "error", listener: (err: Error) => void): this;
  removeListener(event: "finish", listener: () => void): this;
  removeListener(event: "pause", listener: () => void): this;
  removeListener(event: "pipe", listener: (src: Readable) => void): this;
  removeListener(event: "readable", listener: () => void): this;
  removeListener(event: "resume", listener: () => void): this;
  removeListener(event: "unpipe", listener: (src: Readable) => void): this;
}

As you can see, this a long and awkward solution. Unfortunately, TypeScript does not allow me to omit the event methods that are already declared on the base class Transform.

Any ideas how to simplify the code?

Upvotes: 2

Views: 72

Answers (0)

Related Questions