Search code examples
typescripttypescript-generics

Generics for Readable/Writable streams. Infer ReturnReadableStreamReadValue type and ParameterWritableStream type?


I'm trying to infer types from Readable/Writable streams in typescript. So, I did it, but maybe there is a better way for this task, and maybe you can help me with this.

Also I can't figure out, why RsReturnValueType generic performed the wrong typification, any ideas?

Here's the simplified code:

interface Duplex<WriteType extends any = any, ReadType extends any = any> {
    getWritableStream() : WritableStream<WriteType>;

    getReadableStream() : ReadableStream<ReadType>;
}
class DuplexRealizationExample implements Duplex<string, string> {
    getWritableStream(): WritableStream<string> {
        return new WritableStream<string>();
    }
    getReadableStream(): ReadableStream<string> {
        throw new ReadableStream<string>();
    }
}
interface AbstractStreamsAdapter<T extends Duplex> {

    toUnderlyingWritableStream(data : number) : getWsArgumentType<T>;

    fromUnderlyingReadableStream(data : RsReturnValueType<T>["value"]) : number;
}

type RsStream<T extends Duplex> = ReturnType<T["getReadableStream"]>;
type RsReaderType<T extends ReadableStream> = ReturnType<T["getReader"]>;
// todo: is there a way to not use `unknown`?
type RsReadReturnType<T extends ReadableStreamReader<unknown>> = Awaited<ReturnType<T["read"]>>;
type RsReturnValueType<T extends Duplex> = (RsReadReturnType<RsReaderType<RsStream<T>>> & {done : false})["value"];

type WsStream<T extends Duplex> = ReturnType<T["getWritableStream"]>;
type WsReaderType<T extends WritableStream> = ReturnType<T["getWriter"]>;
type getWsArgumentType<T extends Duplex> = Parameters<WsReaderType<WsStream<T>>["write"]>[0];

class StreamAdapterExample implements AbstractStreamsAdapter<DuplexRealizationExample> {
    toUnderlyingWritableStream(data: number): getWsArgumentType<DuplexRealizationExample> {
        return data.toString();
    }

    fromUnderlyingReadableStream(data: RsReturnValueType<DuplexRealizationExample>): number {
        // todo: why `data - string | ArrayBuffer`? It should be just a `string`.
        return parseInt(data);
    }
}

Thank you for your help^


Solution

  • From reading the code I presume that the intent of your chain of utility types is to extract W and R from a value of type Duplex<W, R>. You're drilling down into the structure of those stream types, using indexed accesses and conditional type inference with infer (in the form of ReturnType<T> and Parameters<T>), and trying to tease out the types you care about. But you're not doing it right; there are some interesting things in there like a union with ReadableStreamBYOBReader, a type for which you "Bring Your Own Buffer", meaning it expects there to be an ArrayBuffer, and that gets in the way. You could likely be more careful and filter that out, with more utility types like Extract<T, U> or Exclude<T, U>, but it's just so complicated. And if you're using infer anyway, you might as well do that right at the top and see if it works:

    type DuplexReadType<T extends Duplex> = T extends Duplex<any, infer R> ? R : never
    type DuplexWriteType<T extends Duplex> = T extends Duplex<infer W, any> ? W : never;
    
    interface AbstractStreamsAdapter<T extends Duplex> {
        toUnderlyingWritableStream(data: number): DuplexWriteType<T>;
        fromUnderlyingReadableStream(data: DuplexReadType<T>): number;
    }
    

    This just asks TypeScript directly what the read and write types are for a given subtype of Duplex. Let's see how it behaves:

    type DRERead = DuplexReadType<DuplexRealizationExample>;
    // type DRERead = string
    type DREWrite = DuplexWriteType<DuplexRealizationExample>;
    // type DREWrite = string
    

    Looks good.

    Playground link to code