Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare a Stream-like interface in TypeScript?

The npm package csv-parse implements a Node.JS Stream like API:

const parser = parse();
parser.on('readable', () => {
  let record: any;    // <-- it should be string[]
  while (record = parser.read()) {
    output.push(record);
  }
});

Its type declaration is:

interface parse {
  (options?: options): NodeJS.ReadWriteStream;
} 

The return type of parse is NodeJS.ReadWriteStream, which has a method read returning string | Buffer.

interface ReadWriteStream extends ReadableStream, WritableStream {
  // ...
}

interface ReadableStream extends EventEmitter {
  read(size?: number): string | Buffer;
  // ...
}

By this definition, I can't define the variable let record: any = parser.read() to its actual type string[].

I have tried to modify the type definition of csv-parse like this:

interface ParserStream extends NodeJS.ReadWriteStream {
    read(size?: number): string[];    
}

interface parse {
    (options?: options): ParserStream;
}

It doesn't work because TypeScript not allow to extend an interface and change method's return type.

What I can think of is to change NodeJS Stream to a generic type like:

interface GenericReadableStream <T> extends EventEmitter {
  read(size?: number): T
  // ...
}

interface ReadableStream extends GenericReadableStream <string | Buffer> {}

Then I can define:

interface ParserStream extends GenericReadableStream <string[]> {}

I don't know if it's a good way. It impacts a lot in current Node.JS type definitions.

like image 218
aleung Avatar asked Oct 19 '22 01:10

aleung


1 Answers

To begin with, CsvParser implementation uses steams in object mode, so string | Buffer isn't really a good return type for CsvParser.read.

Fortunately, TypeScript allows to extend an interface and change method's return type to any. So you can change csv-parse type definition to

  interface CsvParser extends NodeJS.ReadWriteStream {
      read(size?: number): any;

allowing this code to compile

let record: string[];  
while (record = parser.read()) {

But this is not strict enough, because this will compile too:

let record: number[];  
while (record = parser.read()) {

To disallow that, you somehow need to declare an interface that changes read return type to string[]. You can't do that in an interface that directly extends ReadWriteStream because string[] is incompatible with string | Buffer. However it's possible if you add intermediate step - an interface that extends ReadWriteStream and has return type for read compatible with both. It can be an intersection type of any and the desired type for which we can use generic parameter T:

interface ObjectStream<T> extends NodeJS.ReadWriteStream {
    read(size?: number): any & T;
}

Then you can have CsvParser extending it:

interface CsvParser extends ObjectStream<string[]> {
    read(size?: number): string[];
}

That way, you still need to modify csv-parser type definitions, but there is no need to change type definitions for node streams.

NOTE: the first attempt at the answer was

  interface CsvParser extends NodeJS.ReadWriteStream {
      read(size?: number): any & string[];

which, as @aleung noticed, is no different from

  interface CsvParser extends NodeJS.ReadWriteStream {
      read(size?: number): any;

because it still allows to assign the result of read to a variable of any type.

like image 136
artem Avatar answered Oct 27 '22 09:10

artem